From 5200fca82f4719806abde9f430b1f3d3a06ae2c2 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 11 Apr 2026 06:09:53 +0800 Subject: [PATCH] Add GPU sorting for gaussian splat rendering --- .../shaders/gaussian-splat-utilities.shader | 66 ++++ .../Passes/BuiltinGaussianSplatPass.h | 12 +- .../Passes/BuiltinGaussianSplatPass.cpp | 298 +++++++++++++++--- .../BuiltinGaussianSplatPassResources.cpp | 78 ++++- .../BuiltinGaussianSplatPassResources.h | 3 + .../unit/test_builtin_forward_pipeline.cpp | 99 ++++++ ..._builtin_gaussian_splat_pass_resources.cpp | 22 ++ 7 files changed, 529 insertions(+), 49 deletions(-) diff --git a/engine/assets/builtin/shaders/gaussian-splat-utilities.shader b/engine/assets/builtin/shaders/gaussian-splat-utilities.shader index bf4a2184..34f522c4 100644 --- a/engine/assets/builtin/shaders/gaussian-splat-utilities.shader +++ b/engine/assets/builtin/shaders/gaussian-splat-utilities.shader @@ -150,9 +150,17 @@ Shader "Builtin Gaussian Splat Utilities" void GaussianSplatPrepareOrderCS(uint3 dispatchThreadId : SV_DispatchThreadID) { const uint splatCount = (uint)gSplatParams.x; + const uint sortCapacity = max((uint)gSplatParams.y, splatCount); const uint index = dispatchThreadId.x; + if (index >= sortCapacity) + { + return; + } + if (index >= splatCount) { + GaussianSplatSortDistances[index] = 0xffffffffu; + GaussianSplatOrderBuffer[index] = 0u; return; } @@ -201,5 +209,63 @@ Shader "Builtin Gaussian Splat Utilities" } 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 GaussianSplatSortDistances; + RWStructuredBuffer 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 + } } } diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h index f09b6d8d..a6789c86 100644 --- a/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h @@ -211,7 +211,8 @@ private: enum class PassLayoutUsage : Core::uint8 { Draw, - PrepareOrder + PrepareOrder, + BitonicSort }; bool EnsureInitialized(const RenderContext& context); @@ -224,6 +225,7 @@ private: const RenderSceneData& sceneData, const Resources::Material* material) const; ResolvedShaderPass ResolvePrepareOrderShaderPass(const RenderSceneData& sceneData) const; + ResolvedShaderPass ResolveBitonicSortShaderPass(const RenderSceneData& sceneData) const; PassResourceLayout* GetOrCreatePassResourceLayout( const RenderContext& context, const ResolvedShaderPass& resolvedShaderPass, @@ -235,7 +237,9 @@ private: const Resources::Material* material); RHI::RHIPipelineState* GetOrCreateComputePipelineState( const RenderContext& context, - const RenderSceneData& sceneData); + const ResolvedShaderPass& resolvedShaderPass, + PassLayoutUsage usage, + const Resources::ShaderKeywordSet& keywordSet); bool CreateOwnedDescriptorSet( const BuiltinPassSetLayoutMetadata& setLayout, OwnedDescriptorSet& descriptorSet); @@ -259,6 +263,10 @@ private: const RenderContext& context, const RenderSceneData& sceneData, const VisibleGaussianSplatItem& visibleGaussianSplat); + bool SortVisibleGaussianSplat( + const RenderContext& context, + const RenderSceneData& sceneData, + const VisibleGaussianSplatItem& visibleGaussianSplat); bool DrawVisibleGaussianSplat( const RenderContext& context, const RenderSurface& surface, diff --git a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp index 84599090..dcfd6e15 100644 --- a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp +++ b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp @@ -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 void BindDescriptorSetRanges( Core::uint32 firstDescriptorSet, @@ -309,6 +330,13 @@ bool BuiltinGaussianSplatPass::Execute(const RenderPassContext& context) { return false; } + if (!SortVisibleGaussianSplat( + context.renderContext, + context.sceneData, + visibleGaussianSplat)) { + return false; + } + if (!DrawVisibleGaussianSplat( context.renderContext, context.surface, @@ -479,6 +507,26 @@ BuiltinGaussianSplatPass::ResolvedShaderPass BuiltinGaussianSplatPass::ResolvePr 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( const RenderContext& context, const ResolvedShaderPass& resolvedShaderPass, @@ -555,6 +603,12 @@ BuiltinGaussianSplatPass::PassResourceLayout* BuiltinGaussianSplatPass::GetOrCre return failLayout( "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 nativeSetLayouts(passLayout.setLayouts.size()); @@ -640,8 +694,9 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreatePipelineState( RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState( const RenderContext& context, - const RenderSceneData& sceneData) { - const ResolvedShaderPass resolvedShaderPass = ResolvePrepareOrderShaderPass(sceneData); + const ResolvedShaderPass& resolvedShaderPass, + PassLayoutUsage usage, + const Resources::ShaderKeywordSet& keywordSet) { if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { return nullptr; } @@ -649,7 +704,7 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState PassResourceLayout* passLayout = GetOrCreatePassResourceLayout( context, resolvedShaderPass, - PassLayoutUsage::PrepareOrder); + usage); if (passLayout == nullptr || passLayout->pipelineLayout == nullptr) { return nullptr; } @@ -658,7 +713,7 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState pipelineKey.shader = resolvedShaderPass.shader; pipelineKey.passName = resolvedShaderPass.passName; pipelineKey.keywordSignature = - ::XCEngine::Rendering::Internal::BuildShaderKeywordSignature(sceneData.globalShaderKeywords); + ::XCEngine::Rendering::Internal::BuildShaderKeywordSignature(keywordSet); const auto existing = m_computePipelineStates.find(pipelineKey); if (existing != m_computePipelineStates.end()) { @@ -672,7 +727,7 @@ RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreateComputePipelineState *resolvedShaderPass.shader, *resolvedShaderPass.pass, resolvedShaderPass.passName, - sceneData.globalShaderKeywords); + keywordSet); RHI::RHIPipelineState* pipelineState = context.device->CreateComputePipelineState(pipelineDesc); if (pipelineState == nullptr || !pipelineState->IsValid()) { const Containers::String error = @@ -985,35 +1040,20 @@ bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat( context, resolvedShaderPass, PassLayoutUsage::PrepareOrder); - RHI::RHIPipelineState* pipelineState = GetOrCreateComputePipelineState(context, sceneData); + RHI::RHIPipelineState* pipelineState = GetOrCreateComputePipelineState( + context, + resolvedShaderPass, + PassLayoutUsage::PrepareOrder, + sceneData.globalShaderKeywords); if (passLayout == nullptr || pipelineState == nullptr) { return fail("BuiltinGaussianSplatPass prepare-order failed: compute pipeline setup was not created"); } RHI::RHICommandList* commandList = context.commandList; - - if (workingSet->sortDistances.currentState != RHI::ResourceStates::UnorderedAccess) { - commandList->TransitionBarrier( - workingSet->sortDistances.unorderedAccessView, - 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; + if (!TransitionWorkingSetBuffer(commandList, workingSet->sortDistances, RHI::ResourceStates::UnorderedAccess) || + !TransitionWorkingSetBuffer(commandList, workingSet->orderIndices, RHI::ResourceStates::UnorderedAccess) || + !TransitionWorkingSetBuffer(commandList, workingSet->viewData, RHI::ResourceStates::UnorderedAccess)) { + return fail("BuiltinGaussianSplatPass prepare-order failed: resource transition failed"); } commandList->SetPipelineState(pipelineState); @@ -1029,7 +1069,11 @@ bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat( static_cast(sceneData.cameraData.viewportHeight), sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast(sceneData.cameraData.viewportWidth) : 0.0f, sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast(sceneData.cameraData.viewportHeight) : 0.0f), - Math::Vector4(static_cast(cachedGaussianSplat->splatCount), 0.0f, 0.0f, 0.0f) + Math::Vector4( + static_cast(cachedGaussianSplat->splatCount), + static_cast(workingSet->sortCapacity), + 0.0f, + 0.0f) }; 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(sceneData.cameraData.viewportWidth), + static_cast(sceneData.cameraData.viewportHeight), + sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast(sceneData.cameraData.viewportWidth) : 0.0f, + sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast(sceneData.cameraData.viewportHeight) : 0.0f), + Math::Vector4( + static_cast(cachedGaussianSplat->splatCount), + static_cast(workingSet->sortCapacity), + static_cast(partnerMask), + static_cast(levelMask)) + }; + + if (passLayout->descriptorSetCount > 0u) { + std::vector 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; } @@ -1206,7 +1416,11 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( static_cast(sceneData.cameraData.viewportHeight), sceneData.cameraData.viewportWidth > 0u ? 1.0f / static_cast(sceneData.cameraData.viewportWidth) : 0.0f, sceneData.cameraData.viewportHeight > 0u ? 1.0f / static_cast(sceneData.cameraData.viewportHeight) : 0.0f), - Math::Vector4(static_cast(cachedGaussianSplat->splatCount), 0.0f, 0.0f, 0.0f) + Math::Vector4( + static_cast(cachedGaussianSplat->splatCount), + static_cast(workingSet->sortCapacity), + 0.0f, + 0.0f) }; if (passLayout->descriptorSetCount > 0u) { diff --git a/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp index a0ad202a..303a41b4 100644 --- a/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp +++ b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp @@ -1,6 +1,7 @@ #include "Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h" #include "Components/GaussianSplatRendererComponent.h" +#include "Debug/Logger.h" #include "Resources/GaussianSplat/GaussianSplat.h" namespace XCEngine { @@ -16,6 +17,9 @@ bool CreateStructuredBufferViews( Core::uint32 elementStride, BuiltinGaussianSplatPassResources::CachedBufferView& bufferView) { if (device == nullptr || elementCount == 0u || elementStride == 0u) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: invalid parameters"); return false; } @@ -23,10 +27,13 @@ bool CreateStructuredBufferViews( bufferDesc.size = static_cast(elementCount) * static_cast(elementStride); bufferDesc.stride = elementStride; bufferDesc.bufferType = static_cast(RHI::BufferType::Storage); - bufferDesc.flags = 0u; + bufferDesc.flags = static_cast(RHI::BufferFlags::AllowUnorderedAccess); bufferView.buffer = device->CreateBuffer(bufferDesc); if (bufferView.buffer == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: CreateBuffer returned null"); return false; } @@ -42,11 +49,17 @@ bool CreateStructuredBufferViews( bufferView.shaderResourceView = device->CreateShaderResourceView(bufferView.buffer, viewDesc); if (bufferView.shaderResourceView == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: CreateShaderResourceView returned null"); return false; } bufferView.unorderedAccessView = device->CreateUnorderedAccessView(bufferView.buffer, viewDesc); if (bufferView.unorderedAccessView == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::CreateStructuredBufferViews failed: CreateUnorderedAccessView returned null"); return false; } @@ -71,15 +84,25 @@ bool BuiltinGaussianSplatPassResources::EnsureWorkingSet( visibleGaussianSplat.gaussianSplatRenderer == nullptr || visibleGaussianSplat.gaussianSplat == nullptr || !visibleGaussianSplat.gaussianSplat->IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::EnsureWorkingSet failed: invalid input"); return false; } const Core::uint32 splatCapacity = visibleGaussianSplat.gaussianSplat->GetSplatCount(); + const Core::uint32 sortCapacity = ComputeSortCapacity(splatCapacity); if (splatCapacity == 0u) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::EnsureWorkingSet failed: splat capacity is zero"); return false; } if (!ResetForDevice(device)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::EnsureWorkingSet failed: ResetForDevice returned false"); return false; } @@ -88,7 +111,10 @@ bool BuiltinGaussianSplatPassResources::EnsureWorkingSet( DestroyBufferView(workingSet.sortDistances); DestroyBufferView(workingSet.orderIndices); 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); return false; } @@ -219,19 +245,49 @@ bool BuiltinGaussianSplatPassResources::ResetForDevice(RHI::RHIDevice* device) { bool BuiltinGaussianSplatPassResources::RecreateWorkingSet( RHI::RHIDevice* device, Core::uint32 splatCapacity, + Core::uint32 sortCapacity, WorkingSet& workingSet) { - if (!CreateStructuredBufferView(device, splatCapacity, kSortDistanceStride, workingSet.sortDistances) || - !CreateStructuredBufferView(device, splatCapacity, kOrderIndexStride, workingSet.orderIndices) || - !CreateStructuredBufferView(device, splatCapacity, kViewDataStride, workingSet.viewData)) { + if (!CreateStructuredBufferView(device, sortCapacity, kSortDistanceStride, workingSet.sortDistances)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPassResources::RecreateWorkingSet failed: sort-distance 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, 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; } workingSet.splatCapacity = splatCapacity; + workingSet.sortCapacity = sortCapacity; return true; } @@ -291,6 +347,18 @@ bool BuiltinGaussianSplatPassResources::RecreateAccumulationSurface( 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 Passes } // namespace Rendering diff --git a/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h index 9a9c030f..aea2180f 100644 --- a/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h +++ b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h @@ -35,6 +35,7 @@ public: struct WorkingSet { const Components::GaussianSplatRendererComponent* renderer = nullptr; Core::uint32 splatCapacity = 0u; + Core::uint32 sortCapacity = 0u; CachedBufferView sortDistances = {}; CachedBufferView orderIndices = {}; CachedBufferView viewData = {}; @@ -81,6 +82,7 @@ private: bool RecreateWorkingSet( RHI::RHIDevice* device, Core::uint32 splatCapacity, + Core::uint32 sortCapacity, WorkingSet& workingSet); bool CreateStructuredBufferView( RHI::RHIDevice* device, @@ -92,6 +94,7 @@ private: Core::uint32 width, Core::uint32 height, RHI::Format format); + static Core::uint32 ComputeSortCapacity(Core::uint32 splatCapacity); RHI::RHIDevice* m_device = nullptr; std::unordered_map m_workingSets; diff --git a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp index dfa07da0..bf3a568e 100644 --- a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp +++ b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp @@ -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(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(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 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(setLayouts[4].bindings[0].type), + DescriptorType::UAV); + EXPECT_EQ( + static_cast(setLayouts[4].bindings[1].type), + DescriptorType::UAV); + + delete shader; +} + TEST(BuiltinForwardPipeline_Test, OpenGLPipelineLayoutUsesUnifiedStorageBufferBindingsForGaussianSplatUtilities) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinGaussianSplatUtilitiesShaderPath()); diff --git a/tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp b/tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp index 9926d96c..9de1183d 100644 --- a/tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp +++ b/tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp @@ -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(); + MockGaussianSplatDevice device(state); + BuiltinGaussianSplatPassResources resources; + + GameObject gameObject("RoundedGaussianSplatObject"); + auto* renderer = gameObject.AddComponent(); + std::unique_ptr 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(); MockGaussianSplatDevice device(state);