Add GPU sorting for gaussian splat rendering

This commit is contained in:
2026-04-11 06:09:53 +08:00
parent 39632e1a04
commit 5200fca82f
7 changed files with 529 additions and 49 deletions

View File

@@ -1163,6 +1163,59 @@ TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatUtilitiesShaderUsesCompute
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatBitonicSortShaderUsesComputeAuthoringContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("GaussianSplatBitonicSort");
ASSERT_NE(pass, nullptr);
EXPECT_EQ(pass->resources.Size(), 3u);
const ShaderResourceBindingDesc* perObject =
shader->FindPassResourceBinding("GaussianSplatBitonicSort", "PerObjectConstants");
ASSERT_NE(perObject, nullptr);
EXPECT_EQ(perObject->type, ShaderResourceType::ConstantBuffer);
EXPECT_EQ(perObject->set, 0u);
EXPECT_EQ(perObject->binding, 0u);
EXPECT_EQ(
ResolveBuiltinPassResourceSemantic(*perObject),
BuiltinPassResourceSemantic::PerObject);
const ShaderResourceBindingDesc* sortDistances =
shader->FindPassResourceBinding("GaussianSplatBitonicSort", "GaussianSplatSortDistances");
ASSERT_NE(sortDistances, nullptr);
EXPECT_EQ(sortDistances->type, ShaderResourceType::RWStructuredBuffer);
EXPECT_EQ(sortDistances->set, 4u);
EXPECT_EQ(sortDistances->binding, 0u);
EXPECT_EQ(
ResolveBuiltinPassResourceSemantic(*sortDistances),
BuiltinPassResourceSemantic::GaussianSplatSortDistanceBuffer);
const ShaderResourceBindingDesc* orderBuffer =
shader->FindPassResourceBinding("GaussianSplatBitonicSort", "GaussianSplatOrderBuffer");
ASSERT_NE(orderBuffer, nullptr);
EXPECT_EQ(orderBuffer->type, ShaderResourceType::RWStructuredBuffer);
EXPECT_EQ(orderBuffer->set, 4u);
EXPECT_EQ(orderBuffer->binding, 1u);
EXPECT_EQ(
ResolveBuiltinPassResourceSemantic(*orderBuffer),
BuiltinPassResourceSemantic::GaussianSplatOrderBuffer);
const ShaderStageVariant* computeVariant = shader->FindVariant(
"GaussianSplatBitonicSort",
XCEngine::Resources::ShaderType::Compute,
XCEngine::Resources::ShaderBackend::D3D12);
ASSERT_NE(computeVariant, nullptr);
EXPECT_EQ(computeVariant->entryPoint, "GaussianSplatBitonicSortCS");
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedGaussianSplatUtilitiesShaderContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath());
@@ -1253,6 +1306,52 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoaded
delete shader;
}
TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedGaussianSplatBitonicSortContract) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath());
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
Shader* shader = static_cast<Shader*>(result.resource);
ASSERT_NE(shader, nullptr);
const ShaderPass* pass = shader->FindPass("GaussianSplatBitonicSort");
ASSERT_NE(pass, nullptr);
BuiltinPassResourceBindingPlan plan = {};
String error;
EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr();
ASSERT_EQ(plan.bindings.Size(), 3u);
EXPECT_TRUE(plan.perObject.IsValid());
EXPECT_TRUE(plan.gaussianSplatSortDistanceBuffer.IsValid());
EXPECT_TRUE(plan.gaussianSplatOrderBuffer.IsValid());
EXPECT_FALSE(plan.gaussianSplatViewDataBuffer.IsValid());
EXPECT_EQ(plan.perObject.set, 0u);
EXPECT_EQ(plan.gaussianSplatSortDistanceBuffer.set, 4u);
EXPECT_EQ(plan.gaussianSplatSortDistanceBuffer.binding, 0u);
EXPECT_EQ(plan.gaussianSplatOrderBuffer.set, 4u);
EXPECT_EQ(plan.gaussianSplatOrderBuffer.binding, 1u);
EXPECT_EQ(plan.firstDescriptorSet, 0u);
EXPECT_EQ(plan.descriptorSetCount, 5u);
std::vector<BuiltinPassSetLayoutMetadata> setLayouts;
ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr();
ASSERT_EQ(setLayouts.size(), 5u);
EXPECT_TRUE(setLayouts[0].usesPerObject);
EXPECT_TRUE(setLayouts[4].usesGaussianSplatSortDistanceBuffer);
EXPECT_TRUE(setLayouts[4].usesGaussianSplatOrderBuffer);
EXPECT_FALSE(setLayouts[4].usesGaussianSplatViewDataBuffer);
ASSERT_EQ(setLayouts[4].bindings.size(), 2u);
EXPECT_EQ(
static_cast<DescriptorType>(setLayouts[4].bindings[0].type),
DescriptorType::UAV);
EXPECT_EQ(
static_cast<DescriptorType>(setLayouts[4].bindings[1].type),
DescriptorType::UAV);
delete shader;
}
TEST(BuiltinForwardPipeline_Test, OpenGLPipelineLayoutUsesUnifiedStorageBufferBindingsForGaussianSplatUtilities) {
ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath());

View File

@@ -335,6 +335,7 @@ TEST(BuiltinGaussianSplatPassResources_Test, EnsureWorkingSetAllocatesAndReusesS
EXPECT_EQ(resources.GetWorkingSetCount(), 1u);
EXPECT_EQ(workingSet->renderer, renderer);
EXPECT_EQ(workingSet->splatCapacity, 8u);
EXPECT_EQ(workingSet->sortCapacity, 8u);
EXPECT_EQ(workingSet->sortDistances.elementStride, sizeof(float));
EXPECT_EQ(workingSet->orderIndices.elementStride, sizeof(XCEngine::Core::uint32));
EXPECT_EQ(workingSet->viewData.elementStride, sizeof(GaussianSplatViewData));
@@ -385,12 +386,33 @@ TEST(BuiltinGaussianSplatPassResources_Test, EnsureWorkingSetKeepsPerRendererIso
ASSERT_NE(grownWorkingSet, nullptr);
EXPECT_EQ(grownWorkingSet, resources.FindWorkingSet(firstRenderer));
EXPECT_EQ(grownWorkingSet->splatCapacity, 12u);
EXPECT_EQ(grownWorkingSet->sortCapacity, 16u);
EXPECT_EQ(resources.GetWorkingSetCount(), 2u);
EXPECT_EQ(state->createBufferCalls, 9);
EXPECT_GE(state->bufferShutdownCalls, 3);
EXPECT_GE(state->bufferDestroyCalls, 3);
}
TEST(BuiltinGaussianSplatPassResources_Test, EnsureWorkingSetRoundsSortBuffersUpToNextPowerOfTwo) {
auto state = std::make_shared<MockGaussianSplatResourceState>();
MockGaussianSplatDevice device(state);
BuiltinGaussianSplatPassResources resources;
GameObject gameObject("RoundedGaussianSplatObject");
auto* renderer = gameObject.AddComponent<GaussianSplatRendererComponent>();
std::unique_ptr<GaussianSplat> gaussianSplat(CreateTestGaussianSplat("GaussianSplats/rounded.xcgsplat", 9u));
VisibleGaussianSplatItem item = BuildVisibleGaussianSplatItem(gameObject, *renderer, *gaussianSplat);
BuiltinGaussianSplatPassResources::WorkingSet* workingSet = nullptr;
ASSERT_TRUE(resources.EnsureWorkingSet(&device, item, workingSet));
ASSERT_NE(workingSet, nullptr);
EXPECT_EQ(workingSet->splatCapacity, 9u);
EXPECT_EQ(workingSet->sortCapacity, 16u);
EXPECT_EQ(workingSet->sortDistances.elementCount, 16u);
EXPECT_EQ(workingSet->orderIndices.elementCount, 16u);
EXPECT_EQ(workingSet->viewData.elementCount, 9u);
}
TEST(BuiltinGaussianSplatPassResources_Test, EnsureAccumulationSurfaceReusesCompatibleTargetAndRecreatesOnResize) {
auto state = std::make_shared<MockGaussianSplatResourceState>();
MockGaussianSplatDevice device(state);