feat(srp): formalize renderer contracts and project feature bridge

This commit is contained in:
2026-04-20 15:03:45 +08:00
parent 10b092d467
commit a615f78e72
13 changed files with 1604 additions and 28 deletions

View File

@@ -3082,6 +3082,164 @@ TEST_F(
nullptr);
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeUsesSameFallbackRendererAcrossBackendRequestPlanAndExecution) {
const auto bridge =
XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge();
ASSERT_NE(bridge, nullptr);
const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedFallbackRendererSelectionConsistencyProbeAsset"
};
std::shared_ptr<const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetRuntime>
assetRuntime = bridge->CreateAssetRuntime(descriptor);
ASSERT_NE(assetRuntime, nullptr);
const std::shared_ptr<const XCEngine::Rendering::RenderPipelineAsset>
rendererAsset = assetRuntime->GetPipelineRendererAsset();
ASSERT_NE(rendererAsset, nullptr);
std::unique_ptr<XCEngine::Rendering::RenderPipeline> rendererPipeline =
rendererAsset->CreatePipeline();
ASSERT_NE(rendererPipeline, nullptr);
EXPECT_NE(
dynamic_cast<XCEngine::Rendering::Pipelines::BuiltinForwardPipeline*>(
rendererPipeline.get()),
nullptr);
Scene* runtimeScene =
CreateScene("ManagedFallbackRendererSelectionConsistencyScene");
GameObject* cameraObject = runtimeScene->CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
ASSERT_NE(camera, nullptr);
camera->SetPrimary(true);
GameObject* lightObject = runtimeScene->CreateGameObject("Light");
auto* light = lightObject->AddComponent<LightComponent>();
ASSERT_NE(light, nullptr);
light->SetLightType(LightType::Directional);
light->SetCastsShadows(true);
XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset
asset(descriptor);
XCEngine::Rendering::CameraRenderRequest request = {};
request.scene = runtimeScene;
request.camera = camera;
request.surface = XCEngine::Rendering::RenderSurface(64u, 64u);
XCEngine::Rendering::ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy(
request,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
ASSERT_TRUE(request.directionalShadow.IsValid());
asset.ConfigureCameraRenderRequest(
request,
0u,
0u,
XCEngine::Rendering::DirectionalShadowPlanningSettings{});
EXPECT_FALSE(request.directionalShadow.IsValid());
XCEngine::Rendering::CameraFramePlan plan =
XCEngine::Rendering::CameraFramePlan::FromRequest(request);
asset.ConfigureCameraFramePlan(plan);
EXPECT_TRUE(
plan.IsFullscreenStageRequested(
XCEngine::Rendering::CameraFrameStage::PostProcess));
EXPECT_EQ(
plan.ResolveStageColorSource(
XCEngine::Rendering::CameraFrameStage::PostProcess),
XCEngine::Rendering::CameraFrameColorSource::MainSceneColor);
std::unique_ptr<XCEngine::Rendering::RenderPipeline> pipeline =
asset.CreatePipeline();
auto* host =
dynamic_cast<XCEngine::Rendering::Pipelines::ScriptableRenderPipelineHost*>(
pipeline.get());
ASSERT_NE(host, nullptr);
ASSERT_NE(host->GetStageRecorder(), nullptr);
EXPECT_TRUE(
host->GetStageRecorder()->Initialize(
XCEngine::Rendering::RenderContext{}))
<< runtime->GetLastError();
EXPECT_TRUE(
host->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene))
<< runtime->GetLastError();
XCEngine::Rendering::RenderGraph graph;
XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph);
XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {};
colorDesc.width = 64u;
colorDesc.height = 64u;
colorDesc.format =
static_cast<XCEngine::Core::uint32>(
XCEngine::RHI::Format::R8G8B8A8_UNorm);
XCEngine::Rendering::RenderGraphTextureDesc depthDesc = colorDesc;
depthDesc.format =
static_cast<XCEngine::Core::uint32>(
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderGraphTextureHandle colorTarget =
graphBuilder.CreateTransientTexture(
"ManagedFallbackSelectedMainSceneColor",
colorDesc);
const XCEngine::Rendering::RenderGraphTextureHandle depthTarget =
graphBuilder.CreateTransientTexture(
"ManagedFallbackSelectedMainSceneDepth",
depthDesc);
const XCEngine::Rendering::RenderSceneData sceneData = {};
const XCEngine::Rendering::RenderSurface surface(64u, 64u);
bool executionSucceeded = true;
XCEngine::Rendering::RenderGraphBlackboard blackboard = {};
const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = {
graphBuilder,
"ManagedFallbackSelectedMainScene",
XCEngine::Rendering::CameraFrameStage::MainScene,
{},
sceneData,
surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{ colorTarget },
depthTarget,
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(host->GetStageRecorder()->RecordStageRenderGraph(graphContext))
<< runtime->GetLastError();
XCEngine::Rendering::CompiledRenderGraph compiledGraph = {};
XCEngine::Containers::String errorMessage;
ASSERT_TRUE(
XCEngine::Rendering::RenderGraphCompiler::Compile(
graph,
compiledGraph,
&errorMessage))
<< errorMessage.CStr();
ASSERT_EQ(compiledGraph.GetPassCount(), 3u);
EXPECT_STREQ(
compiledGraph.GetPassName(0).CStr(),
"ManagedFallbackSelectedMainScene.Opaque");
EXPECT_STREQ(
compiledGraph.GetPassName(1).CStr(),
"ManagedFallbackSelectedMainScene.Skybox");
EXPECT_STREQ(
compiledGraph.GetPassName(2).CStr(),
"ManagedFallbackSelectedMainScene.Transparent");
host->GetStageRecorder()->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeFallsBackToDefaultSceneRecorderWhenBackendKeyIsUnknown) {
@@ -3304,9 +3462,21 @@ TEST_F(
int observedCreateRendererCallCount = 0;
int observedSetupRendererCallCount = 0;
int observedCreateFeatureCallCount = 0;
int observedCreatePipelineCallCount = 0;
int observedDisposePipelineCallCount = 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",
@@ -3331,17 +3501,130 @@ TEST_F(
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(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeReleasesPersistentRendererFeatureAcrossRendererInvalidationAndAssetRuntimeRelease) {
Scene* runtimeScene =
CreateScene("ManagedPersistentFeatureLifecycleScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ManagedPersistentFeatureSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedPersistentFeatureRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* observationObject =
runtimeScene->CreateGameObject(
"ManagedPersistentFeatureObservation");
ScriptComponent* observationScript =
AddScript(
observationObject,
"Gameplay",
"ManagedPersistentFeatureObservationProbe");
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, "ManagedPersistentFeatureProbeAsset");
{
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(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeRebuildsPipelineAfterAssetInvalidation) {
@@ -3532,6 +3815,116 @@ TEST_F(
recorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedRenderPipelineBridgeKeepsBuiltinAndCustomFeaturePassOrderStable) {
Scene* runtimeScene =
CreateScene("ManagedFeaturePassOrderScene");
GameObject* selectionObject =
runtimeScene->CreateGameObject(
"ManagedFeaturePassOrderSelection");
ScriptComponent* selectionScript =
AddScript(
selectionObject,
"Gameplay",
"ManagedFeaturePassOrderRuntimeSelectionProbe");
ASSERT_NE(selectionScript, nullptr);
GameObject* observationObject =
runtimeScene->CreateGameObject(
"ManagedFeaturePassOrderObservation");
ScriptComponent* observationScript =
AddScript(
observationObject,
"Gameplay",
"ManagedFeaturePassOrderObservationProbe");
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, "ManagedFeaturePassOrderProbeAsset");
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 renderContext = {};
ASSERT_TRUE(recorder->Initialize(renderContext));
ASSERT_TRUE(
recorder->SupportsStageRenderGraph(
XCEngine::Rendering::CameraFrameStage::MainScene));
XCEngine::Rendering::RenderGraph graph;
XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph);
XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {};
colorDesc.width = 64u;
colorDesc.height = 64u;
colorDesc.format =
static_cast<XCEngine::Core::uint32>(
XCEngine::RHI::Format::R8G8B8A8_UNorm);
XCEngine::Rendering::RenderGraphTextureDesc depthDesc = colorDesc;
depthDesc.format =
static_cast<XCEngine::Core::uint32>(
XCEngine::RHI::Format::D32_Float);
const XCEngine::Rendering::RenderGraphTextureHandle colorTarget =
graphBuilder.CreateTransientTexture(
"ManagedFeaturePassOrderColor",
colorDesc);
const XCEngine::Rendering::RenderGraphTextureHandle depthTarget =
graphBuilder.CreateTransientTexture(
"ManagedFeaturePassOrderDepth",
depthDesc);
const XCEngine::Rendering::RenderSceneData sceneData = {};
const XCEngine::Rendering::RenderSurface surface(64u, 64u);
bool executionSucceeded = true;
XCEngine::Rendering::RenderGraphBlackboard blackboard = {};
const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = {
graphBuilder,
"ManagedFeaturePassOrder",
XCEngine::Rendering::CameraFrameStage::MainScene,
renderContext,
sceneData,
surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{ colorTarget },
depthTarget,
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(recorder->RecordStageRenderGraph(graphContext));
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError();
std::string observedOrder;
EXPECT_TRUE(runtime->TryGetFieldValue(
observationScript,
"ObservedOrder",
observedOrder));
EXPECT_EQ(observedOrder, "Builtin>CustomA>CustomB");
recorder->Shutdown();
}
TEST_F(
MonoScriptRuntimeTest,
ManagedStageRecorderRecordsShaderVectorPostProcessThroughScriptableRenderContext) {