diff --git a/docs/plan/SRP_项目侧自定义渲染管线打通计划_2026-04-20.md b/docs/used/SRP_项目侧自定义渲染管线打通计划_完成归档_2026-04-20.md similarity index 100% rename from docs/plan/SRP_项目侧自定义渲染管线打通计划_2026-04-20.md rename to docs/used/SRP_项目侧自定义渲染管线打通计划_完成归档_2026-04-20.md diff --git a/project/Assets/Scripts/ProjectRenderPipelineProbe.cs b/project/Assets/Scripts/ProjectRenderPipelineProbe.cs index 31e9be52..87612c98 100644 --- a/project/Assets/Scripts/ProjectRenderPipelineProbe.cs +++ b/project/Assets/Scripts/ProjectRenderPipelineProbe.cs @@ -1,9 +1,38 @@ +using System.Reflection; using XCEngine; using XCEngine.Rendering; using XCEngine.Rendering.Universal; namespace ProjectScripts { + internal static class ProjectProbeRuntimeVersionUtility + { + private static readonly MethodInfo s_getRuntimeResourceVersionMethod = + typeof(ScriptableRenderPipelineAsset) + .GetMethod( + "GetRuntimeResourceVersionInstance", + BindingFlags.Instance | + BindingFlags.NonPublic); + + public static int GetRuntimeResourceVersion( + ScriptableRenderPipelineAsset asset) + { + if (asset == null || + s_getRuntimeResourceVersionMethod == null) + { + return 0; + } + + object version = + s_getRuntimeResourceVersionMethod.Invoke( + asset, + null); + return version is int resolvedVersion + ? resolvedVersion + : 0; + } + } + public sealed class ProjectPostProcessColorScalePass : ScriptableRenderPass { @@ -154,4 +183,549 @@ namespace ProjectScripts }; } } + + internal static class ProjectRendererInvalidationProbeState + { + public static int CreatePipelineCallCount; + public static int DisposePipelineCallCount; + public static int CreateRendererCallCount; + public static int SetupRendererCallCount; + public static int CreateFeatureCallCount; + public static int DisposeRendererCallCount; + public static int DisposeFeatureCallCount; + public static int InvalidateRendererCallCount; + + public static void Reset() + { + CreatePipelineCallCount = 0; + DisposePipelineCallCount = 0; + CreateRendererCallCount = 0; + SetupRendererCallCount = 0; + CreateFeatureCallCount = 0; + DisposeRendererCallCount = 0; + DisposeFeatureCallCount = 0; + InvalidateRendererCallCount = 0; + } + } + + public sealed class ProjectRendererInvalidationProbePipeline + : RendererBackedRenderPipeline + { + public ProjectRendererInvalidationProbePipeline( + RendererBackedRenderPipelineAsset asset) + : base(asset) + { + ProjectRendererInvalidationProbeState + .CreatePipelineCallCount++; + } + + protected override void Dispose( + bool disposing) + { + if (disposing) + { + ProjectRendererInvalidationProbeState + .DisposePipelineCallCount++; + } + + base.Dispose(disposing); + } + } + + public sealed class ProjectRendererInvalidationProbeFeature + : ScriptableRendererFeature + { + protected override void ReleaseRuntimeResources() + { + ProjectRendererInvalidationProbeState + .DisposeFeatureCallCount++; + } + } + + public sealed class ProjectRendererInvalidationProbeRenderer + : ScriptableRenderer + { + protected override bool SupportsRendererRecording( + RendererRecordingContext context) + { + return context != null && + context.stage == CameraFrameStage.MainScene; + } + + protected override bool RecordRenderer( + RendererRecordingContext context) + { + return context != null; + } + + protected override void ReleaseRuntimeResources() + { + ProjectRendererInvalidationProbeState + .DisposeRendererCallCount++; + } + } + + public sealed class ProjectRendererInvalidationProbeRendererData + : ScriptableRendererData + { + protected override ScriptableRenderer CreateRenderer() + { + ProjectRendererInvalidationProbeState + .CreateRendererCallCount++; + return new ProjectRendererInvalidationProbeRenderer(); + } + + protected override void SetupRenderer( + ScriptableRenderer renderer) + { + ProjectRendererInvalidationProbeState + .SetupRendererCallCount++; + base.SetupRenderer(renderer); + } + + protected override ScriptableRendererFeature[] + CreateRendererFeatures() + { + ProjectRendererInvalidationProbeState + .CreateFeatureCallCount++; + return new ScriptableRendererFeature[] + { + new ProjectRendererInvalidationProbeFeature() + }; + } + + protected override string GetPipelineRendererAssetKey() + { + return "BuiltinForward"; + } + + public void InvalidateForTest() + { + ProjectRendererInvalidationProbeState + .InvalidateRendererCallCount++; + SetDirty(); + } + } + + public sealed class ProjectRendererInvalidationProbeAsset + : RendererBackedRenderPipelineAsset + { + private readonly ProjectRendererInvalidationProbeRendererData + m_rendererData; + + public ProjectRendererInvalidationProbeAsset() + { + ProjectRendererInvalidationProbeState.Reset(); + m_rendererData = + new ProjectRendererInvalidationProbeRendererData(); + rendererDataList = new ScriptableRendererData[] + { + m_rendererData + }; + } + + protected override ScriptableRenderPipeline + CreateRendererBackedPipeline() + { + return new ProjectRendererInvalidationProbePipeline( + this); + } + + public void InvalidateDefaultRendererForTest() + { + if (m_rendererData == null) + { + return; + } + + m_rendererData.InvalidateForTest(); + } + } + + internal static class ProjectPersistentFeatureProbeState + { + public static int CreateRendererCallCount; + public static int CreateFeatureRuntimeCallCount; + public static int DisposeRendererCallCount; + public static int DisposeFeatureCallCount; + public static int InvalidateRendererCallCount; + + public static void Reset() + { + CreateRendererCallCount = 0; + CreateFeatureRuntimeCallCount = 0; + DisposeRendererCallCount = 0; + DisposeFeatureCallCount = 0; + InvalidateRendererCallCount = 0; + } + } + + public sealed class ProjectPersistentFeatureProbeRendererFeature + : ScriptableRendererFeature + { + public override void Create() + { + ProjectPersistentFeatureProbeState + .CreateFeatureRuntimeCallCount++; + } + + protected override void ReleaseRuntimeResources() + { + ProjectPersistentFeatureProbeState + .DisposeFeatureCallCount++; + } + } + + public sealed class ProjectPersistentFeatureProbeRenderer + : ScriptableRenderer + { + protected override bool SupportsRendererRecording( + RendererRecordingContext context) + { + return context != null && + context.stage == CameraFrameStage.MainScene; + } + + protected override bool RecordRenderer( + RendererRecordingContext context) + { + return context != null; + } + + protected override void ReleaseRuntimeResources() + { + ProjectPersistentFeatureProbeState + .DisposeRendererCallCount++; + } + } + + public sealed class ProjectPersistentFeatureProbeRendererData + : ScriptableRendererData + { + private readonly ProjectPersistentFeatureProbeRendererFeature + m_feature = + new ProjectPersistentFeatureProbeRendererFeature(); + + protected override ScriptableRenderer CreateRenderer() + { + ProjectPersistentFeatureProbeState + .CreateRendererCallCount++; + return new ProjectPersistentFeatureProbeRenderer(); + } + + protected override ScriptableRendererFeature[] + CreateRendererFeatures() + { + return new ScriptableRendererFeature[] + { + m_feature + }; + } + + protected override string GetPipelineRendererAssetKey() + { + return "BuiltinForward"; + } + + public void InvalidateForTest() + { + ProjectPersistentFeatureProbeState + .InvalidateRendererCallCount++; + SetDirty(); + } + } + + public sealed class ProjectPersistentFeatureProbeAsset + : RendererBackedRenderPipelineAsset + { + private readonly ProjectPersistentFeatureProbeRendererData + m_rendererData; + + public ProjectPersistentFeatureProbeAsset() + { + ProjectPersistentFeatureProbeState.Reset(); + m_rendererData = + new ProjectPersistentFeatureProbeRendererData(); + rendererDataList = new ScriptableRendererData[] + { + m_rendererData + }; + } + + public void InvalidateDefaultRendererForTest() + { + if (m_rendererData == null) + { + return; + } + + m_rendererData.InvalidateForTest(); + } + } + + internal static class ProjectAssetInvalidationProbeState + { + 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; + } + } + + public sealed class ProjectAssetInvalidationProbePipeline + : ScriptableRenderPipeline + { + private readonly CameraFrameStage m_supportedStage; + + public ProjectAssetInvalidationProbePipeline( + CameraFrameStage supportedStage) + { + m_supportedStage = supportedStage; + ProjectAssetInvalidationProbeState + .LastCreatedSupportedStage = + (int)supportedStage; + } + + protected override bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + return stage == m_supportedStage; + } + + protected override bool RecordStageRenderGraph( + ScriptableRenderContext context) + { + return context != null; + } + + protected override void Dispose( + bool disposing) + { + if (disposing) + { + ProjectAssetInvalidationProbeState + .DisposePipelineCallCount++; + } + } + } + + public sealed class ProjectAssetInvalidationProbeAsset + : ScriptableRenderPipelineAsset + { + private CameraFrameStage m_supportedStage = + CameraFrameStage.MainScene; + + public ProjectAssetInvalidationProbeAsset() + { + ProjectAssetInvalidationProbeState.Reset(); + } + + protected override ScriptableRenderPipeline CreatePipeline() + { + ProjectAssetInvalidationProbeState + .CreatePipelineCallCount++; + return new ProjectAssetInvalidationProbePipeline( + m_supportedStage); + } + + public void InvalidatePipelineForTest( + CameraFrameStage supportedStage) + { + if (m_supportedStage == supportedStage) + { + return; + } + + m_supportedStage = supportedStage; + ProjectAssetInvalidationProbeState + .InvalidateAssetCallCount++; + SetDirty(); + } + } + + public sealed class ProjectRendererInvalidationRuntimeSelectionProbe + : MonoBehaviour + { + public void Start() + { + GraphicsSettings.renderPipelineAsset = + new ProjectRendererInvalidationProbeAsset(); + } + } + + public sealed class ProjectRendererInvalidationObservationProbe + : MonoBehaviour + { + public int ObservedCreatePipelineCallCount; + public int ObservedDisposePipelineCallCount; + public int ObservedCreateRendererCallCount; + public int ObservedSetupRendererCallCount; + public int ObservedCreateFeatureCallCount; + public int ObservedDisposeRendererCallCount; + public int ObservedDisposeFeatureCallCount; + public int ObservedInvalidateRendererCallCount; + public int ObservedRuntimeResourceVersionBeforeInvalidation; + public int ObservedRuntimeResourceVersionAfterInvalidation; + + private bool m_requestedInvalidation; + + public void Update() + { + ProjectRendererInvalidationProbeAsset selectedAsset = + GraphicsSettings.renderPipelineAsset + as ProjectRendererInvalidationProbeAsset; + if (!m_requestedInvalidation && + selectedAsset != null && + ProjectRendererInvalidationProbeState + .CreateRendererCallCount > 0) + { + ObservedRuntimeResourceVersionBeforeInvalidation = + ProjectProbeRuntimeVersionUtility + .GetRuntimeResourceVersion( + selectedAsset); + selectedAsset.InvalidateDefaultRendererForTest(); + ObservedRuntimeResourceVersionAfterInvalidation = + ProjectProbeRuntimeVersionUtility + .GetRuntimeResourceVersion( + selectedAsset); + m_requestedInvalidation = true; + } + + ObservedCreatePipelineCallCount = + ProjectRendererInvalidationProbeState + .CreatePipelineCallCount; + ObservedDisposePipelineCallCount = + ProjectRendererInvalidationProbeState + .DisposePipelineCallCount; + ObservedCreateRendererCallCount = + ProjectRendererInvalidationProbeState + .CreateRendererCallCount; + ObservedSetupRendererCallCount = + ProjectRendererInvalidationProbeState + .SetupRendererCallCount; + ObservedCreateFeatureCallCount = + ProjectRendererInvalidationProbeState + .CreateFeatureCallCount; + ObservedDisposeRendererCallCount = + ProjectRendererInvalidationProbeState + .DisposeRendererCallCount; + ObservedDisposeFeatureCallCount = + ProjectRendererInvalidationProbeState + .DisposeFeatureCallCount; + ObservedInvalidateRendererCallCount = + ProjectRendererInvalidationProbeState + .InvalidateRendererCallCount; + } + } + + public sealed class ProjectPersistentFeatureRuntimeSelectionProbe + : MonoBehaviour + { + public void Start() + { + GraphicsSettings.renderPipelineAsset = + new ProjectPersistentFeatureProbeAsset(); + } + } + + public sealed class ProjectPersistentFeatureObservationProbe + : MonoBehaviour + { + public int ObservedCreateRendererCallCount; + public int ObservedCreateFeatureRuntimeCallCount; + public int ObservedDisposeRendererCallCount; + public int ObservedDisposeFeatureCallCount; + public int ObservedInvalidateRendererCallCount; + + private bool m_requestedInvalidation; + + public void Update() + { + ProjectPersistentFeatureProbeAsset selectedAsset = + GraphicsSettings.renderPipelineAsset + as ProjectPersistentFeatureProbeAsset; + if (!m_requestedInvalidation && + selectedAsset != null && + ProjectPersistentFeatureProbeState + .CreateFeatureRuntimeCallCount > 0) + { + selectedAsset.InvalidateDefaultRendererForTest(); + m_requestedInvalidation = true; + } + + ObservedCreateRendererCallCount = + ProjectPersistentFeatureProbeState + .CreateRendererCallCount; + ObservedCreateFeatureRuntimeCallCount = + ProjectPersistentFeatureProbeState + .CreateFeatureRuntimeCallCount; + ObservedDisposeRendererCallCount = + ProjectPersistentFeatureProbeState + .DisposeRendererCallCount; + ObservedDisposeFeatureCallCount = + ProjectPersistentFeatureProbeState + .DisposeFeatureCallCount; + ObservedInvalidateRendererCallCount = + ProjectPersistentFeatureProbeState + .InvalidateRendererCallCount; + } + } + + public sealed class ProjectAssetInvalidationRuntimeSelectionProbe + : MonoBehaviour + { + public void Start() + { + GraphicsSettings.renderPipelineAsset = + new ProjectAssetInvalidationProbeAsset(); + } + } + + public sealed class ProjectAssetInvalidationObservationProbe + : MonoBehaviour + { + public int ObservedCreatePipelineCallCount; + public int ObservedDisposePipelineCallCount; + public int ObservedInvalidateAssetCallCount; + public int ObservedLastCreatedSupportedStage; + + private bool m_requestedInvalidation; + + public void Update() + { + ProjectAssetInvalidationProbeAsset selectedAsset = + GraphicsSettings.renderPipelineAsset + as ProjectAssetInvalidationProbeAsset; + if (!m_requestedInvalidation && + selectedAsset != null && + ProjectAssetInvalidationProbeState + .CreatePipelineCallCount > 0) + { + selectedAsset.InvalidatePipelineForTest( + CameraFrameStage.PostProcess); + m_requestedInvalidation = true; + } + + ObservedCreatePipelineCallCount = + ProjectAssetInvalidationProbeState + .CreatePipelineCallCount; + ObservedDisposePipelineCallCount = + ProjectAssetInvalidationProbeState + .DisposePipelineCallCount; + ObservedInvalidateAssetCallCount = + ProjectAssetInvalidationProbeState + .InvalidateAssetCallCount; + ObservedLastCreatedSupportedStage = + ProjectAssetInvalidationProbeState + .LastCreatedSupportedStage; + } + } } diff --git a/tests/scripting/test_project_script_assembly.cpp b/tests/scripting/test_project_script_assembly.cpp index 94d82b58..fc14078a 100644 --- a/tests/scripting/test_project_script_assembly.cpp +++ b/tests/scripting/test_project_script_assembly.cpp @@ -1,12 +1,17 @@ #include +#include +#include #include #include #include #include #include #include +#include #include +#include +#include #include #include @@ -19,6 +24,7 @@ #include #endif +using namespace XCEngine::Components; using namespace XCEngine::Scripting; namespace { @@ -77,6 +83,15 @@ MonoScriptRuntime::Settings CreateProjectMonoSettings() { class ProjectScriptAssemblyTest : public ::testing::Test { protected: void SetUp() override { + engine = &ScriptEngine::Get(); + engine->OnRuntimeStop(); + engine->SetRuntimeFixedDeltaTime( + ScriptEngine::DefaultFixedDeltaTime); + XCEngine::Input::InputManager::Get().Shutdown(); + XCEngine::Resources::ResourceManager::Get().Shutdown(); + XCEngine::Rendering::Pipelines:: + ClearConfiguredManagedRenderPipelineAssetDescriptor(); + ASSERT_TRUE(std::filesystem::exists(ResolveProjectScriptCoreDllPath())); ASSERT_TRUE(std::filesystem::exists(ResolveProjectGameScriptsDllPath())); @@ -97,9 +112,43 @@ protected: runtime = std::make_unique(std::move(settings)); ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError(); + engine->SetRuntime(runtime.get()); } + void TearDown() override { + if (engine != nullptr) { + engine->OnRuntimeStop(); + engine->SetRuntime(nullptr); + } + + XCEngine::Input::InputManager::Get().Shutdown(); + XCEngine::Rendering::Pipelines:: + ClearConfiguredManagedRenderPipelineAssetDescriptor(); + runtime.reset(); + scene.reset(); + XCEngine::Resources::ResourceManager::Get().Shutdown(); + } + + Scene* CreateScene(const std::string& sceneName) { + scene = std::make_unique(sceneName); + return scene.get(); + } + + ScriptComponent* AddProjectScript( + GameObject* gameObject, + const std::string& className) { + ScriptComponent* component = + gameObject->AddComponent(); + component->SetScriptClass( + "GameScripts", + "ProjectScripts", + className); + return component; + } + + ScriptEngine* engine = nullptr; std::unique_ptr runtime; + std::unique_ptr scene; }; TEST_F(ProjectScriptAssemblyTest, InitializesFromProjectScriptAssemblyDirectory) { @@ -360,4 +409,338 @@ TEST_F( recorder->Shutdown(); } +TEST_F( + ProjectScriptAssemblyTest, + ProjectManagedBridgeRebuildsRendererAfterProjectRendererDataInvalidation) { + Scene* runtimeScene = + CreateScene("ProjectRendererInvalidationScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ProjectRendererInvalidationSelection"); + ScriptComponent* selectionScript = + AddProjectScript( + selectionObject, + "ProjectRendererInvalidationRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + GameObject* observationObject = + runtimeScene->CreateGameObject( + "ProjectRendererInvalidationObservation"); + ScriptComponent* observationScript = + AddProjectScript( + observationObject, + "ProjectRendererInvalidationObservationProbe"); + 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, "ProjectScripts"); + EXPECT_EQ( + descriptor.className, + "ProjectRendererInvalidationProbeAsset"); + + 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)); + + engine->OnUpdate(0.016f); + + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreatePipelineCallCount = 0; + int observedDisposePipelineCallCount = 0; + int observedCreateRendererCallCount = 0; + int observedSetupRendererCallCount = 0; + int observedCreateFeatureCallCount = 0; + int observedDisposeRendererCallCount = 0; + int observedDisposeFeatureCallCount = 0; + int observedInvalidateRendererCallCount = 0; + int observedRuntimeResourceVersionBeforeInvalidation = 0; + int observedRuntimeResourceVersionAfterInvalidation = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreatePipelineCallCount", + observedCreatePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposePipelineCallCount", + observedDisposePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedSetupRendererCallCount", + observedSetupRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateFeatureCallCount", + observedCreateFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeRendererCallCount", + observedDisposeRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeFeatureCallCount", + observedDisposeFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedInvalidateRendererCallCount", + observedInvalidateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedRuntimeResourceVersionBeforeInvalidation", + observedRuntimeResourceVersionBeforeInvalidation)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedRuntimeResourceVersionAfterInvalidation", + observedRuntimeResourceVersionAfterInvalidation)); + + EXPECT_EQ(observedCreatePipelineCallCount, 2); + EXPECT_EQ(observedDisposePipelineCallCount, 1); + EXPECT_EQ(observedCreateRendererCallCount, 2); + EXPECT_EQ(observedSetupRendererCallCount, 2); + EXPECT_EQ(observedCreateFeatureCallCount, 2); + EXPECT_EQ(observedDisposeRendererCallCount, 1); + EXPECT_EQ(observedDisposeFeatureCallCount, 1); + EXPECT_EQ(observedInvalidateRendererCallCount, 1); + EXPECT_GT(observedRuntimeResourceVersionBeforeInvalidation, 0); + EXPECT_EQ( + observedRuntimeResourceVersionAfterInvalidation, + observedRuntimeResourceVersionBeforeInvalidation + 1); + + recorder->Shutdown(); +} + +TEST_F( + ProjectScriptAssemblyTest, + ProjectManagedBridgeReleasesProjectRendererCachesAcrossInvalidationAndAssetRuntimeRelease) { + Scene* runtimeScene = + CreateScene("ProjectPersistentFeatureLifecycleScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ProjectPersistentFeatureSelection"); + ScriptComponent* selectionScript = + AddProjectScript( + selectionObject, + "ProjectPersistentFeatureRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + GameObject* observationObject = + runtimeScene->CreateGameObject( + "ProjectPersistentFeatureObservation"); + ScriptComponent* observationScript = + AddProjectScript( + observationObject, + "ProjectPersistentFeatureObservationProbe"); + 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, "ProjectScripts"); + EXPECT_EQ( + descriptor.className, + "ProjectPersistentFeatureProbeAsset"); + + { + 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)); + + engine->OnUpdate(0.016f); + + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + recorder->Shutdown(); + } + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreateRendererCallCount = 0; + int observedCreateFeatureRuntimeCallCount = 0; + int observedDisposeRendererCallCount = 0; + int observedDisposeFeatureCallCount = 0; + int observedInvalidateRendererCallCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateFeatureRuntimeCallCount", + observedCreateFeatureRuntimeCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeRendererCallCount", + observedDisposeRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeFeatureCallCount", + observedDisposeFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedInvalidateRendererCallCount", + observedInvalidateRendererCallCount)); + + EXPECT_EQ(observedCreateRendererCallCount, 2); + EXPECT_EQ(observedCreateFeatureRuntimeCallCount, 2); + EXPECT_EQ(observedDisposeRendererCallCount, 2); + EXPECT_EQ(observedDisposeFeatureCallCount, 2); + EXPECT_EQ(observedInvalidateRendererCallCount, 1); +} + +TEST_F( + ProjectScriptAssemblyTest, + ProjectManagedBridgeRebuildsPipelineAfterProjectAssetInvalidation) { + Scene* runtimeScene = + CreateScene("ProjectAssetInvalidationScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ProjectAssetInvalidationSelection"); + ScriptComponent* selectionScript = + AddProjectScript( + selectionObject, + "ProjectAssetInvalidationRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + GameObject* observationObject = + runtimeScene->CreateGameObject( + "ProjectAssetInvalidationObservation"); + ScriptComponent* observationScript = + AddProjectScript( + observationObject, + "ProjectAssetInvalidationObservationProbe"); + 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, "ProjectScripts"); + EXPECT_EQ( + descriptor.className, + "ProjectAssetInvalidationProbeAsset"); + + 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(); +} + } // namespace