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

@@ -150,9 +150,17 @@ Shader "Builtin Gaussian Splat Utilities"
void GaussianSplatPrepareOrderCS(uint3 dispatchThreadId : SV_DispatchThreadID) void GaussianSplatPrepareOrderCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{ {
const uint splatCount = (uint)gSplatParams.x; const uint splatCount = (uint)gSplatParams.x;
const uint sortCapacity = max((uint)gSplatParams.y, splatCount);
const uint index = dispatchThreadId.x; const uint index = dispatchThreadId.x;
if (index >= sortCapacity)
{
return;
}
if (index >= splatCount) if (index >= splatCount)
{ {
GaussianSplatSortDistances[index] = 0xffffffffu;
GaussianSplatOrderBuffer[index] = 0u;
return; return;
} }
@@ -201,5 +209,63 @@ Shader "Builtin Gaussian Splat Utilities"
} }
ENDHLSL ENDHLSL
} }
Pass
{
Name "GaussianSplatBitonicSort"
HLSLPROGRAM
#pragma target 4.5
#pragma compute GaussianSplatBitonicSortCS
cbuffer PerObjectConstants
{
float4x4 gProjectionMatrix;
float4x4 gViewMatrix;
float4x4 gModelMatrix;
float4 gCameraRight;
float4 gCameraUp;
float4 gScreenParams;
float4 gSplatParams;
};
RWStructuredBuffer<uint> GaussianSplatSortDistances;
RWStructuredBuffer<uint> GaussianSplatOrderBuffer;
[numthreads(256, 1, 1)]
void GaussianSplatBitonicSortCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
const uint sortCapacity = (uint)gSplatParams.y;
const uint partnerMask = (uint)gSplatParams.z;
const uint levelMask = (uint)gSplatParams.w;
const uint index = dispatchThreadId.x;
if (index >= sortCapacity)
{
return;
}
const uint partnerIndex = index ^ partnerMask;
if (partnerIndex >= sortCapacity || partnerIndex <= index)
{
return;
}
const uint leftDistance = GaussianSplatSortDistances[index];
const uint rightDistance = GaussianSplatSortDistances[partnerIndex];
const uint leftOrder = GaussianSplatOrderBuffer[index];
const uint rightOrder = GaussianSplatOrderBuffer[partnerIndex];
const bool ascending = (index & levelMask) == 0u;
const bool shouldSwap = ascending ? (leftDistance > rightDistance) : (leftDistance < rightDistance);
if (!shouldSwap)
{
return;
}
GaussianSplatSortDistances[index] = rightDistance;
GaussianSplatSortDistances[partnerIndex] = leftDistance;
GaussianSplatOrderBuffer[index] = rightOrder;
GaussianSplatOrderBuffer[partnerIndex] = leftOrder;
}
ENDHLSL
}
} }
} }

View File

@@ -211,7 +211,8 @@ private:
enum class PassLayoutUsage : Core::uint8 { enum class PassLayoutUsage : Core::uint8 {
Draw, Draw,
PrepareOrder PrepareOrder,
BitonicSort
}; };
bool EnsureInitialized(const RenderContext& context); bool EnsureInitialized(const RenderContext& context);
@@ -224,6 +225,7 @@ private:
const RenderSceneData& sceneData, const RenderSceneData& sceneData,
const Resources::Material* material) const; const Resources::Material* material) const;
ResolvedShaderPass ResolvePrepareOrderShaderPass(const RenderSceneData& sceneData) const; ResolvedShaderPass ResolvePrepareOrderShaderPass(const RenderSceneData& sceneData) const;
ResolvedShaderPass ResolveBitonicSortShaderPass(const RenderSceneData& sceneData) const;
PassResourceLayout* GetOrCreatePassResourceLayout( PassResourceLayout* GetOrCreatePassResourceLayout(
const RenderContext& context, const RenderContext& context,
const ResolvedShaderPass& resolvedShaderPass, const ResolvedShaderPass& resolvedShaderPass,
@@ -235,7 +237,9 @@ private:
const Resources::Material* material); const Resources::Material* material);
RHI::RHIPipelineState* GetOrCreateComputePipelineState( RHI::RHIPipelineState* GetOrCreateComputePipelineState(
const RenderContext& context, const RenderContext& context,
const RenderSceneData& sceneData); const ResolvedShaderPass& resolvedShaderPass,
PassLayoutUsage usage,
const Resources::ShaderKeywordSet& keywordSet);
bool CreateOwnedDescriptorSet( bool CreateOwnedDescriptorSet(
const BuiltinPassSetLayoutMetadata& setLayout, const BuiltinPassSetLayoutMetadata& setLayout,
OwnedDescriptorSet& descriptorSet); OwnedDescriptorSet& descriptorSet);
@@ -259,6 +263,10 @@ private:
const RenderContext& context, const RenderContext& context,
const RenderSceneData& sceneData, const RenderSceneData& sceneData,
const VisibleGaussianSplatItem& visibleGaussianSplat); const VisibleGaussianSplatItem& visibleGaussianSplat);
bool SortVisibleGaussianSplat(
const RenderContext& context,
const RenderSceneData& sceneData,
const VisibleGaussianSplatItem& visibleGaussianSplat);
bool DrawVisibleGaussianSplat( bool DrawVisibleGaussianSplat(
const RenderContext& context, const RenderContext& context,
const RenderSurface& surface, const RenderSurface& surface,

View File

@@ -158,6 +158,27 @@ RHI::RHIResourceView* ResolveWorkingSetView(
} }
} }
bool TransitionWorkingSetBuffer(
RHI::RHICommandList* commandList,
Internal::BuiltinGaussianSplatPassResources::CachedBufferView& bufferView,
RHI::ResourceStates targetState) {
if (commandList == nullptr || bufferView.currentState == targetState) {
return true;
}
RHI::RHIResourceView* resourceView =
targetState == RHI::ResourceStates::UnorderedAccess
? bufferView.unorderedAccessView
: bufferView.shaderResourceView;
if (resourceView == nullptr) {
return false;
}
commandList->TransitionBarrier(resourceView, bufferView.currentState, targetState);
bufferView.currentState = targetState;
return true;
}
template <typename BindFn> template <typename BindFn>
void BindDescriptorSetRanges( void BindDescriptorSetRanges(
Core::uint32 firstDescriptorSet, Core::uint32 firstDescriptorSet,
@@ -309,6 +330,13 @@ bool BuiltinGaussianSplatPass::Execute(const RenderPassContext& context) {
return false; return false;
} }
if (!SortVisibleGaussianSplat(
context.renderContext,
context.sceneData,
visibleGaussianSplat)) {
return false;
}
if (!DrawVisibleGaussianSplat( if (!DrawVisibleGaussianSplat(
context.renderContext, context.renderContext,
context.surface, context.surface,
@@ -479,6 +507,26 @@ BuiltinGaussianSplatPass::ResolvedShaderPass BuiltinGaussianSplatPass::ResolvePr
return resolved; return resolved;
} }
BuiltinGaussianSplatPass::ResolvedShaderPass BuiltinGaussianSplatPass::ResolveBitonicSortShaderPass(
const RenderSceneData& sceneData) const {
ResolvedShaderPass resolved = {};
if (!m_builtinGaussianSplatUtilitiesShader.IsValid()) {
return resolved;
}
const Resources::Shader* shader = m_builtinGaussianSplatUtilitiesShader.Get();
const Resources::ShaderBackend backend = ::XCEngine::Rendering::Internal::ToShaderBackend(m_backendType);
const Containers::String passName("GaussianSplatBitonicSort");
if (const Resources::ShaderPass* shaderPass =
FindCompatibleComputePass(*shader, passName, sceneData.globalShaderKeywords, backend)) {
resolved.shader = shader;
resolved.pass = shaderPass;
resolved.passName = passName;
}
return resolved;
}
BuiltinGaussianSplatPass::PassResourceLayout* BuiltinGaussianSplatPass::GetOrCreatePassResourceLayout( BuiltinGaussianSplatPass::PassResourceLayout* BuiltinGaussianSplatPass::GetOrCreatePassResourceLayout(
const RenderContext& context, const RenderContext& context,
const ResolvedShaderPass& resolvedShaderPass, const ResolvedShaderPass& resolvedShaderPass,
@@ -555,6 +603,12 @@ BuiltinGaussianSplatPass::PassResourceLayout* BuiltinGaussianSplatPass::GetOrCre
return failLayout( return failLayout(
"BuiltinGaussianSplatPass prepare-order pass requires sort distance, order, position, other, color, and view-data gaussian splat buffer bindings"); "BuiltinGaussianSplatPass prepare-order pass requires sort distance, order, position, other, color, and view-data gaussian splat buffer bindings");
} }
} else if (usage == PassLayoutUsage::BitonicSort) {
if (!passLayout.gaussianSplatSortDistanceBuffer.IsValid() ||
!passLayout.gaussianSplatOrderBuffer.IsValid()) {
return failLayout(
"BuiltinGaussianSplatPass bitonic-sort pass requires sort distance and order gaussian splat buffer bindings");
}
} }
std::vector<RHI::DescriptorSetLayoutDesc> nativeSetLayouts(passLayout.setLayouts.size()); std::vector<RHI::DescriptorSetLayoutDesc> nativeSetLayouts(passLayout.setLayouts.size());
@@ -640,8 +694,9 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreatePipelineState(
RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState( RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState(
const RenderContext& context, const RenderContext& context,
const RenderSceneData& sceneData) { const ResolvedShaderPass& resolvedShaderPass,
const ResolvedShaderPass resolvedShaderPass = ResolvePrepareOrderShaderPass(sceneData); PassLayoutUsage usage,
const Resources::ShaderKeywordSet& keywordSet) {
if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) {
return nullptr; return nullptr;
} }
@@ -649,7 +704,7 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState
PassResourceLayout* passLayout = GetOrCreatePassResourceLayout( PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(
context, context,
resolvedShaderPass, resolvedShaderPass,
PassLayoutUsage::PrepareOrder); usage);
if (passLayout == nullptr || passLayout->pipelineLayout == nullptr) { if (passLayout == nullptr || passLayout->pipelineLayout == nullptr) {
return nullptr; return nullptr;
} }
@@ -658,7 +713,7 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState
pipelineKey.shader = resolvedShaderPass.shader; pipelineKey.shader = resolvedShaderPass.shader;
pipelineKey.passName = resolvedShaderPass.passName; pipelineKey.passName = resolvedShaderPass.passName;
pipelineKey.keywordSignature = pipelineKey.keywordSignature =
::XCEngine::Rendering::Internal::BuildShaderKeywordSignature(sceneData.globalShaderKeywords); ::XCEngine::Rendering::Internal::BuildShaderKeywordSignature(keywordSet);
const auto existing = m_computePipelineStates.find(pipelineKey); const auto existing = m_computePipelineStates.find(pipelineKey);
if (existing != m_computePipelineStates.end()) { if (existing != m_computePipelineStates.end()) {
@@ -672,7 +727,7 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState
*resolvedShaderPass.shader, *resolvedShaderPass.shader,
*resolvedShaderPass.pass, *resolvedShaderPass.pass,
resolvedShaderPass.passName, resolvedShaderPass.passName,
sceneData.globalShaderKeywords); keywordSet);
RHI::RHIPipelineState* pipelineState = context.device->CreateComputePipelineState(pipelineDesc); RHI::RHIPipelineState* pipelineState = context.device->CreateComputePipelineState(pipelineDesc);
if (pipelineState == nullptr || !pipelineState->IsValid()) { if (pipelineState == nullptr || !pipelineState->IsValid()) {
const Containers::String error = const Containers::String error =
@@ -985,35 +1040,20 @@ bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat(
context, context,
resolvedShaderPass, resolvedShaderPass,
PassLayoutUsage::PrepareOrder); PassLayoutUsage::PrepareOrder);
RHI::RHIPipelineState* pipelineState = GetOrCreateComputePipelineState(context, sceneData); RHI::RHIPipelineState* pipelineState = GetOrCreateComputePipelineState(
context,
resolvedShaderPass,
PassLayoutUsage::PrepareOrder,
sceneData.globalShaderKeywords);
if (passLayout == nullptr || pipelineState == nullptr) { if (passLayout == nullptr || pipelineState == nullptr) {
return fail("BuiltinGaussianSplatPass prepare-order failed: compute pipeline setup was not created"); return fail("BuiltinGaussianSplatPass prepare-order failed: compute pipeline setup was not created");
} }
RHI::RHICommandList* commandList = context.commandList; RHI::RHICommandList* commandList = context.commandList;
if (!TransitionWorkingSetBuffer(commandList, workingSet->sortDistances, RHI::ResourceStates::UnorderedAccess) ||
if (workingSet->sortDistances.currentState != RHI::ResourceStates::UnorderedAccess) { !TransitionWorkingSetBuffer(commandList, workingSet->orderIndices, RHI::ResourceStates::UnorderedAccess) ||
commandList->TransitionBarrier( !TransitionWorkingSetBuffer(commandList, workingSet->viewData, RHI::ResourceStates::UnorderedAccess)) {
workingSet->sortDistances.unorderedAccessView, return fail("BuiltinGaussianSplatPass prepare-order failed: resource transition failed");
workingSet->sortDistances.currentState,
RHI::ResourceStates::UnorderedAccess);
workingSet->sortDistances.currentState = RHI::ResourceStates::UnorderedAccess;
}
if (workingSet->orderIndices.currentState != RHI::ResourceStates::UnorderedAccess) {
commandList->TransitionBarrier(
workingSet->orderIndices.unorderedAccessView,
workingSet->orderIndices.currentState,
RHI::ResourceStates::UnorderedAccess);
workingSet->orderIndices.currentState = RHI::ResourceStates::UnorderedAccess;
}
if (workingSet->viewData.currentState != RHI::ResourceStates::UnorderedAccess) {
commandList->TransitionBarrier(
workingSet->viewData.unorderedAccessView,
workingSet->viewData.currentState,
RHI::ResourceStates::UnorderedAccess);
workingSet->viewData.currentState = RHI::ResourceStates::UnorderedAccess;
} }
commandList->SetPipelineState(pipelineState); commandList->SetPipelineState(pipelineState);
@@ -1029,7 +1069,11 @@ bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat(
static_cast<float>(sceneData.cameraData.viewportHeight), static_cast<float>(sceneData.cameraData.viewportHeight),
sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportWidth) : 0.0f, sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportWidth) : 0.0f,
sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportHeight) : 0.0f), sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportHeight) : 0.0f),
Math::Vector4(static_cast<float>(cachedGaussianSplat->splatCount), 0.0f, 0.0f, 0.0f) Math::Vector4(
static_cast<float>(cachedGaussianSplat->splatCount),
static_cast<float>(workingSet->sortCapacity),
0.0f,
0.0f)
}; };
if (passLayout->descriptorSetCount > 0u) { if (passLayout->descriptorSetCount > 0u) {
@@ -1112,18 +1156,184 @@ bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat(
}); });
} }
commandList->Dispatch((cachedGaussianSplat->splatCount + 63u) / 64u, 1u, 1u); commandList->Dispatch((workingSet->sortCapacity + 63u) / 64u, 1u, 1u);
if (!TransitionWorkingSetBuffer(commandList, workingSet->viewData, RHI::ResourceStates::NonPixelShaderResource)) {
return fail("BuiltinGaussianSplatPass prepare-order failed: view-data transition failed");
}
return true;
}
bool BuiltinGaussianSplatPass::SortVisibleGaussianSplat(
const RenderContext& context,
const RenderSceneData& sceneData,
const VisibleGaussianSplatItem& visibleGaussianSplat) {
auto fail = [](const char* message) -> bool {
Debug::Logger::Get().Error(Debug::LogCategory::Rendering, message);
return false;
};
if (visibleGaussianSplat.gameObject == nullptr ||
visibleGaussianSplat.gaussianSplat == nullptr ||
!visibleGaussianSplat.gaussianSplat->IsValid() ||
m_passResources == nullptr) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: invalid visible gaussian splat item");
}
const RenderResourceCache::CachedGaussianSplat* cachedGaussianSplat =
m_resourceCache.GetOrCreateGaussianSplat(m_device, visibleGaussianSplat.gaussianSplat);
if (cachedGaussianSplat == nullptr) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: gaussian splat GPU cache is missing");
}
Internal::BuiltinGaussianSplatPassResources::WorkingSet* workingSet = nullptr;
if (!m_passResources->EnsureWorkingSet(m_device, visibleGaussianSplat, workingSet) ||
workingSet == nullptr ||
workingSet->sortDistances.shaderResourceView == nullptr ||
workingSet->sortDistances.unorderedAccessView == nullptr ||
workingSet->orderIndices.shaderResourceView == nullptr ||
workingSet->orderIndices.unorderedAccessView == nullptr) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: working set allocation is incomplete");
}
RHI::RHICommandList* commandList = context.commandList;
if (workingSet->sortCapacity <= 1u) {
return TransitionWorkingSetBuffer(
commandList,
workingSet->orderIndices,
RHI::ResourceStates::NonPixelShaderResource);
}
const ResolvedShaderPass resolvedShaderPass = ResolveBitonicSortShaderPass(sceneData);
if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: utilities shader pass was not resolved");
}
PassLayoutKey passLayoutKey = {};
passLayoutKey.shader = resolvedShaderPass.shader;
passLayoutKey.passName = resolvedShaderPass.passName;
PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(
context,
resolvedShaderPass,
PassLayoutUsage::BitonicSort);
RHI::RHIPipelineState* pipelineState = GetOrCreateComputePipelineState(
context,
resolvedShaderPass,
PassLayoutUsage::BitonicSort,
sceneData.globalShaderKeywords);
if (passLayout == nullptr || pipelineState == nullptr) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: compute pipeline setup was not created");
}
commandList->SetPipelineState(pipelineState);
for (Core::uint32 levelMask = 2u; levelMask <= workingSet->sortCapacity; levelMask <<= 1u) {
for (Core::uint32 partnerMask = levelMask >> 1u; partnerMask > 0u; partnerMask >>= 1u) {
if (!TransitionWorkingSetBuffer(commandList, workingSet->sortDistances, RHI::ResourceStates::UnorderedAccess) ||
!TransitionWorkingSetBuffer(commandList, workingSet->orderIndices, RHI::ResourceStates::UnorderedAccess)) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: resource transition to UAV failed");
}
const PerObjectConstants perObjectConstants = {
sceneData.cameraData.projection,
sceneData.cameraData.view,
visibleGaussianSplat.localToWorld.Transpose(),
Math::Vector4(sceneData.cameraData.worldRight, 0.0f),
Math::Vector4(sceneData.cameraData.worldUp, 0.0f),
Math::Vector4(
static_cast<float>(sceneData.cameraData.viewportWidth),
static_cast<float>(sceneData.cameraData.viewportHeight),
sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportWidth) : 0.0f,
sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportHeight) : 0.0f),
Math::Vector4(
static_cast<float>(cachedGaussianSplat->splatCount),
static_cast<float>(workingSet->sortCapacity),
static_cast<float>(partnerMask),
static_cast<float>(levelMask))
};
if (passLayout->descriptorSetCount > 0u) {
std::vector<RHI::RHIDescriptorSet*> descriptorSets(passLayout->descriptorSetCount, nullptr);
for (Core::uint32 descriptorOffset = 0u; descriptorOffset < passLayout->descriptorSetCount; ++descriptorOffset) {
const Core::uint32 setIndex = passLayout->firstDescriptorSet + descriptorOffset;
if (setIndex >= passLayout->setLayouts.size()) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: descriptor set index overflow");
}
const BuiltinPassSetLayoutMetadata& setLayout = passLayout->setLayouts[setIndex];
if (setLayout.layout.bindingCount == 0u) {
continue;
}
if (!(setLayout.usesPerObject ||
setLayout.usesGaussianSplatSortDistanceBuffer ||
setLayout.usesGaussianSplatOrderBuffer)) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: unexpected descriptor set layout");
}
const Core::uint64 objectId =
setLayout.usesPerObject ? visibleGaussianSplat.gameObject->GetID() : 0u;
const Resources::GaussianSplat* gaussianSplatKey =
(setLayout.usesGaussianSplatSortDistanceBuffer ||
setLayout.usesGaussianSplatOrderBuffer)
? visibleGaussianSplat.gaussianSplat
: nullptr;
CachedDescriptorSet* cachedDescriptorSet = GetOrCreateDynamicDescriptorSet(
passLayoutKey,
*passLayout,
setLayout,
setIndex,
objectId,
visibleGaussianSplat.gaussianSplatRenderer,
nullptr,
gaussianSplatKey,
MaterialConstantPayloadView(),
*cachedGaussianSplat,
workingSet->sortDistances.unorderedAccessView,
workingSet->orderIndices.unorderedAccessView,
nullptr);
if (cachedDescriptorSet == nullptr || cachedDescriptorSet->descriptorSet.set == nullptr) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: dynamic descriptor set resolution failed");
}
RHI::RHIDescriptorSet* descriptorSet = cachedDescriptorSet->descriptorSet.set;
if (setLayout.usesPerObject) {
if (!passLayout->perObject.IsValid() || passLayout->perObject.set != setIndex) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: per-object binding is invalid");
}
descriptorSet->WriteConstant(
passLayout->perObject.binding,
&perObjectConstants,
sizeof(perObjectConstants));
}
descriptorSets[descriptorOffset] = descriptorSet;
}
BindDescriptorSetRanges(
passLayout->firstDescriptorSet,
descriptorSets,
[commandList, passLayout](Core::uint32 firstSet, Core::uint32 count, RHI::RHIDescriptorSet** sets) {
commandList->SetComputeDescriptorSets(
firstSet,
count,
sets,
passLayout->pipelineLayout);
});
}
commandList->Dispatch((workingSet->sortCapacity + 255u) / 256u, 1u, 1u);
if (!TransitionWorkingSetBuffer(commandList, workingSet->sortDistances, RHI::ResourceStates::NonPixelShaderResource) ||
!TransitionWorkingSetBuffer(commandList, workingSet->orderIndices, RHI::ResourceStates::NonPixelShaderResource)) {
return fail("BuiltinGaussianSplatPass bitonic-sort failed: resource transition to SRV failed");
}
}
}
commandList->TransitionBarrier(
workingSet->orderIndices.shaderResourceView,
RHI::ResourceStates::UnorderedAccess,
RHI::ResourceStates::NonPixelShaderResource);
workingSet->orderIndices.currentState = RHI::ResourceStates::NonPixelShaderResource;
commandList->TransitionBarrier(
workingSet->viewData.shaderResourceView,
RHI::ResourceStates::UnorderedAccess,
RHI::ResourceStates::NonPixelShaderResource);
workingSet->viewData.currentState = RHI::ResourceStates::NonPixelShaderResource;
return true; return true;
} }
@@ -1206,7 +1416,11 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat(
static_cast<float>(sceneData.cameraData.viewportHeight), static_cast<float>(sceneData.cameraData.viewportHeight),
sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportWidth) : 0.0f, sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportWidth) : 0.0f,
sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportHeight) : 0.0f), sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast<float>(sceneData.cameraData.viewportHeight) : 0.0f),
Math::Vector4(static_cast<float>(cachedGaussianSplat->splatCount), 0.0f, 0.0f, 0.0f) Math::Vector4(
static_cast<float>(cachedGaussianSplat->splatCount),
static_cast<float>(workingSet->sortCapacity),
0.0f,
0.0f)
}; };
if (passLayout->descriptorSetCount > 0u) { if (passLayout->descriptorSetCount > 0u) {

View File

@@ -1,6 +1,7 @@
#include "Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h" #include "Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h"
#include "Components/GaussianSplatRendererComponent.h" #include "Components/GaussianSplatRendererComponent.h"
#include "Debug/Logger.h"
#include "Resources/GaussianSplat/GaussianSplat.h" #include "Resources/GaussianSplat/GaussianSplat.h"
namespace XCEngine { namespace XCEngine {
@@ -16,6 +17,9 @@ bool CreateStructuredBufferViews(
Core::uint32 elementStride, Core::uint32 elementStride,
BuiltinGaussianSplatPassResources::CachedBufferView& bufferView) { BuiltinGaussianSplatPassResources::CachedBufferView& bufferView) {
if (device == nullptr || elementCount == 0u || elementStride == 0u) { if (device == nullptr || elementCount == 0u || elementStride == 0u) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: invalid parameters");
return false; return false;
} }
@@ -23,10 +27,13 @@ bool CreateStructuredBufferViews(
bufferDesc.size = static_cast<Core::uint64>(elementCount) * static_cast<Core::uint64>(elementStride); bufferDesc.size = static_cast<Core::uint64>(elementCount) * static_cast<Core::uint64>(elementStride);
bufferDesc.stride = elementStride; bufferDesc.stride = elementStride;
bufferDesc.bufferType = static_cast<uint32_t>(RHI::BufferType::Storage); bufferDesc.bufferType = static_cast<uint32_t>(RHI::BufferType::Storage);
bufferDesc.flags = 0u; bufferDesc.flags = static_cast<Core::uint64>(RHI::BufferFlags::AllowUnorderedAccess);
bufferView.buffer = device->CreateBuffer(bufferDesc); bufferView.buffer = device->CreateBuffer(bufferDesc);
if (bufferView.buffer == nullptr) { if (bufferView.buffer == nullptr) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: CreateBuffer returned null");
return false; return false;
} }
@@ -42,11 +49,17 @@ bool CreateStructuredBufferViews(
bufferView.shaderResourceView = device->CreateShaderResourceView(bufferView.buffer, viewDesc); bufferView.shaderResourceView = device->CreateShaderResourceView(bufferView.buffer, viewDesc);
if (bufferView.shaderResourceView == nullptr) { if (bufferView.shaderResourceView == nullptr) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: CreateShaderResourceView returned null");
return false; return false;
} }
bufferView.unorderedAccessView = device->CreateUnorderedAccessView(bufferView.buffer, viewDesc); bufferView.unorderedAccessView = device->CreateUnorderedAccessView(bufferView.buffer, viewDesc);
if (bufferView.unorderedAccessView == nullptr) { if (bufferView.unorderedAccessView == nullptr) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: CreateUnorderedAccessView returned null");
return false; return false;
} }
@@ -71,15 +84,25 @@ bool BuiltinGaussianSplatPassResources::EnsureWorkingSet(
visibleGaussianSplat.gaussianSplatRenderer == nullptr || visibleGaussianSplat.gaussianSplatRenderer == nullptr ||
visibleGaussianSplat.gaussianSplat == nullptr || visibleGaussianSplat.gaussianSplat == nullptr ||
!visibleGaussianSplat.gaussianSplat->IsValid()) { !visibleGaussianSplat.gaussianSplat->IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::EnsureWorkingSet failed: invalid input");
return false; return false;
} }
const Core::uint32 splatCapacity = visibleGaussianSplat.gaussianSplat->GetSplatCount(); const Core::uint32 splatCapacity = visibleGaussianSplat.gaussianSplat->GetSplatCount();
const Core::uint32 sortCapacity = ComputeSortCapacity(splatCapacity);
if (splatCapacity == 0u) { if (splatCapacity == 0u) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::EnsureWorkingSet failed: splat capacity is zero");
return false; return false;
} }
if (!ResetForDevice(device)) { if (!ResetForDevice(device)) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::EnsureWorkingSet failed: ResetForDevice returned false");
return false; return false;
} }
@@ -88,7 +111,10 @@ bool BuiltinGaussianSplatPassResources::EnsureWorkingSet(
DestroyBufferView(workingSet.sortDistances); DestroyBufferView(workingSet.sortDistances);
DestroyBufferView(workingSet.orderIndices); DestroyBufferView(workingSet.orderIndices);
DestroyBufferView(workingSet.viewData); DestroyBufferView(workingSet.viewData);
if (!RecreateWorkingSet(device, splatCapacity, workingSet)) { if (!RecreateWorkingSet(device, splatCapacity, sortCapacity, workingSet)) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::EnsureWorkingSet failed: RecreateWorkingSet returned false");
m_workingSets.erase(visibleGaussianSplat.gaussianSplatRenderer); m_workingSets.erase(visibleGaussianSplat.gaussianSplatRenderer);
return false; return false;
} }
@@ -219,19 +245,49 @@ bool BuiltinGaussianSplatPassResources::ResetForDevice(RHI::RHIDevice* device) {
bool BuiltinGaussianSplatPassResources::RecreateWorkingSet( bool BuiltinGaussianSplatPassResources::RecreateWorkingSet(
RHI::RHIDevice* device, RHI::RHIDevice* device,
Core::uint32 splatCapacity, Core::uint32 splatCapacity,
Core::uint32 sortCapacity,
WorkingSet& workingSet) { WorkingSet& workingSet) {
if (!CreateStructuredBufferView(device, splatCapacity, kSortDistanceStride, workingSet.sortDistances) || if (!CreateStructuredBufferView(device, sortCapacity, kSortDistanceStride, workingSet.sortDistances)) {
!CreateStructuredBufferView(device, splatCapacity, kOrderIndexStride, workingSet.orderIndices) || Debug::Logger::Get().Error(
!CreateStructuredBufferView(device, splatCapacity, kViewDataStride, workingSet.viewData)) { Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::RecreateWorkingSet failed: sort-distance buffer view creation failed");
DestroyBufferView(workingSet.sortDistances); DestroyBufferView(workingSet.sortDistances);
DestroyBufferView(workingSet.orderIndices); DestroyBufferView(workingSet.orderIndices);
DestroyBufferView(workingSet.viewData); DestroyBufferView(workingSet.viewData);
workingSet.renderer = nullptr; workingSet.renderer = nullptr;
workingSet.splatCapacity = 0u; workingSet.splatCapacity = 0u;
workingSet.sortCapacity = 0u;
return false;
}
if (!CreateStructuredBufferView(device, sortCapacity, kOrderIndexStride, workingSet.orderIndices)) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::RecreateWorkingSet failed: order-index buffer view creation failed");
DestroyBufferView(workingSet.sortDistances);
DestroyBufferView(workingSet.orderIndices);
DestroyBufferView(workingSet.viewData);
workingSet.renderer = nullptr;
workingSet.splatCapacity = 0u;
workingSet.sortCapacity = 0u;
return false;
}
if (!CreateStructuredBufferView(device, splatCapacity, kViewDataStride, workingSet.viewData)) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"BuiltinGaussianSplatPassResources::RecreateWorkingSet failed: view-data buffer view creation failed");
DestroyBufferView(workingSet.sortDistances);
DestroyBufferView(workingSet.orderIndices);
DestroyBufferView(workingSet.viewData);
workingSet.renderer = nullptr;
workingSet.splatCapacity = 0u;
workingSet.sortCapacity = 0u;
return false; return false;
} }
workingSet.splatCapacity = splatCapacity; workingSet.splatCapacity = splatCapacity;
workingSet.sortCapacity = sortCapacity;
return true; return true;
} }
@@ -291,6 +347,18 @@ bool BuiltinGaussianSplatPassResources::RecreateAccumulationSurface(
return true; return true;
} }
Core::uint32 BuiltinGaussianSplatPassResources::ComputeSortCapacity(Core::uint32 splatCapacity) {
if (splatCapacity == 0u) {
return 0u;
}
Core::uint32 sortCapacity = 1u;
while (sortCapacity < splatCapacity && sortCapacity <= (0x80000000u >> 1u)) {
sortCapacity <<= 1u;
}
return sortCapacity < splatCapacity ? splatCapacity : sortCapacity;
}
} // namespace Internal } // namespace Internal
} // namespace Passes } // namespace Passes
} // namespace Rendering } // namespace Rendering

View File

@@ -35,6 +35,7 @@ public:
struct WorkingSet { struct WorkingSet {
const Components::GaussianSplatRendererComponent* renderer = nullptr; const Components::GaussianSplatRendererComponent* renderer = nullptr;
Core::uint32 splatCapacity = 0u; Core::uint32 splatCapacity = 0u;
Core::uint32 sortCapacity = 0u;
CachedBufferView sortDistances = {}; CachedBufferView sortDistances = {};
CachedBufferView orderIndices = {}; CachedBufferView orderIndices = {};
CachedBufferView viewData = {}; CachedBufferView viewData = {};
@@ -81,6 +82,7 @@ private:
bool RecreateWorkingSet( bool RecreateWorkingSet(
RHI::RHIDevice* device, RHI::RHIDevice* device,
Core::uint32 splatCapacity, Core::uint32 splatCapacity,
Core::uint32 sortCapacity,
WorkingSet& workingSet); WorkingSet& workingSet);
bool CreateStructuredBufferView( bool CreateStructuredBufferView(
RHI::RHIDevice* device, RHI::RHIDevice* device,
@@ -92,6 +94,7 @@ private:
Core::uint32 width, Core::uint32 width,
Core::uint32 height, Core::uint32 height,
RHI::Format format); RHI::Format format);
static Core::uint32 ComputeSortCapacity(Core::uint32 splatCapacity);
RHI::RHIDevice* m_device = nullptr; RHI::RHIDevice* m_device = nullptr;
std::unordered_map<const Components::GaussianSplatRendererComponent*, WorkingSet> m_workingSets; std::unordered_map<const Components::GaussianSplatRendererComponent*, WorkingSet> m_workingSets;

View File

@@ -1163,6 +1163,59 @@ TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatUtilitiesShaderUsesCompute
delete shader; 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) { TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedGaussianSplatUtilitiesShaderContract) {
ShaderLoader loader; ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath()); LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath());
@@ -1253,6 +1306,52 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoaded
delete shader; 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) { TEST(BuiltinForwardPipeline_Test, OpenGLPipelineLayoutUsesUnifiedStorageBufferBindingsForGaussianSplatUtilities) {
ShaderLoader loader; ShaderLoader loader;
LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath()); LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath());

View File

@@ -335,6 +335,7 @@ TEST(BuiltinGaussianSplatPassResources_Test, EnsureWorkingSetAllocatesAndReusesS
EXPECT_EQ(resources.GetWorkingSetCount(), 1u); EXPECT_EQ(resources.GetWorkingSetCount(), 1u);
EXPECT_EQ(workingSet->renderer, renderer); EXPECT_EQ(workingSet->renderer, renderer);
EXPECT_EQ(workingSet->splatCapacity, 8u); EXPECT_EQ(workingSet->splatCapacity, 8u);
EXPECT_EQ(workingSet->sortCapacity, 8u);
EXPECT_EQ(workingSet->sortDistances.elementStride, sizeof(float)); EXPECT_EQ(workingSet->sortDistances.elementStride, sizeof(float));
EXPECT_EQ(workingSet->orderIndices.elementStride, sizeof(XCEngine::Core::uint32)); EXPECT_EQ(workingSet->orderIndices.elementStride, sizeof(XCEngine::Core::uint32));
EXPECT_EQ(workingSet->viewData.elementStride, sizeof(GaussianSplatViewData)); EXPECT_EQ(workingSet->viewData.elementStride, sizeof(GaussianSplatViewData));
@@ -385,12 +386,33 @@ TEST(BuiltinGaussianSplatPassResources_Test, EnsureWorkingSetKeepsPerRendererIso
ASSERT_NE(grownWorkingSet, nullptr); ASSERT_NE(grownWorkingSet, nullptr);
EXPECT_EQ(grownWorkingSet, resources.FindWorkingSet(firstRenderer)); EXPECT_EQ(grownWorkingSet, resources.FindWorkingSet(firstRenderer));
EXPECT_EQ(grownWorkingSet->splatCapacity, 12u); EXPECT_EQ(grownWorkingSet->splatCapacity, 12u);
EXPECT_EQ(grownWorkingSet->sortCapacity, 16u);
EXPECT_EQ(resources.GetWorkingSetCount(), 2u); EXPECT_EQ(resources.GetWorkingSetCount(), 2u);
EXPECT_EQ(state->createBufferCalls, 9); EXPECT_EQ(state->createBufferCalls, 9);
EXPECT_GE(state->bufferShutdownCalls, 3); EXPECT_GE(state->bufferShutdownCalls, 3);
EXPECT_GE(state->bufferDestroyCalls, 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) { TEST(BuiltinGaussianSplatPassResources_Test, EnsureAccumulationSurfaceReusesCompatibleTargetAndRecreatesOnResize) {
auto state = std::make_shared<MockGaussianSplatResourceState>(); auto state = std::make_shared<MockGaussianSplatResourceState>();
MockGaussianSplatDevice device(state); MockGaussianSplatDevice device(state);