feat(srp): lock project-side pipeline lifecycle contracts

Add project asset probes for renderer invalidation, asset invalidation, and runtime release through the public SRP API surface.

Cover the project/Assets bridge path with lifecycle scripting tests and archive the completed project-side SRP bridge plan.
This commit is contained in:
2026-04-20 15:26:33 +08:00
parent 3bdd45b590
commit 3e32f82e37
3 changed files with 957 additions and 0 deletions

View File

@@ -1,12 +1,17 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Rendering/Execution/CameraFrameRenderGraphFrameData.h>
#include <XCEngine/Rendering/Graph/RenderGraph.h>
#include <XCEngine/Rendering/Graph/RenderGraphCompiler.h>
#include <XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <algorithm>
#include <filesystem>
@@ -19,6 +24,7 @@
#include <windows.h>
#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<MonoScriptRuntime>(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<Scene>(sceneName);
return scene.get();
}
ScriptComponent* AddProjectScript(
GameObject* gameObject,
const std::string& className) {
ScriptComponent* component =
gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass(
"GameScripts",
"ProjectScripts",
className);
return component;
}
ScriptEngine* engine = nullptr;
std::unique_ptr<MonoScriptRuntime> runtime;
std::unique_ptr<Scene> 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<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
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<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
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<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipelineStageRecorder>
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<int>(
XCEngine::Rendering::CameraFrameStage::PostProcess));
recorder->Shutdown();
}
} // namespace