From 107b320aa73a8d613439e1b9830ecf349fd8c7ca Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 10 Apr 2026 23:11:11 +0800 Subject: [PATCH] Add builtin GaussianSplat forward pass baseline --- engine/CMakeLists.txt | 2 + .../builtin/shaders/gaussian-splat.shader | 113 +++ .../Builtin/BuiltinPassLayoutUtils.h | 42 + .../Builtin/BuiltinPassMetadataUtils.h | 40 + .../Rendering/Builtin/BuiltinPassTypes.h | 16 + .../Passes/BuiltinGaussianSplatPass.h | 225 ++++++ .../Pipelines/BuiltinForwardPipeline.h | 2 + .../XCEngine/Resources/BuiltinResources.h | 1 + .../Passes/BuiltinGaussianSplatPass.cpp | 730 ++++++++++++++++++ .../Pipelines/BuiltinForwardPipeline.cpp | 21 + engine/src/Resources/BuiltinResources.cpp | 20 + .../unit/test_builtin_forward_pipeline.cpp | 449 ++++++++++- 12 files changed, 1642 insertions(+), 19 deletions(-) create mode 100644 engine/assets/builtin/shaders/gaussian-splat.shader create mode 100644 engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h create mode 100644 engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 71ce84ce..0b99f88d 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -508,6 +508,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Passes/BuiltinInfiniteGridPass.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Passes/BuiltinObjectIdOutlinePass.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Passes/BuiltinSelectionOutlinePass.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Passes/BuiltinVolumetricPass.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Execution/CameraRenderer.cpp @@ -525,6 +526,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinInfiniteGridPass.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinObjectIdOutlinePass.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinSelectionOutlinePass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinVolumetricPass.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSurface.cpp diff --git a/engine/assets/builtin/shaders/gaussian-splat.shader b/engine/assets/builtin/shaders/gaussian-splat.shader new file mode 100644 index 00000000..44e735f0 --- /dev/null +++ b/engine/assets/builtin/shaders/gaussian-splat.shader @@ -0,0 +1,113 @@ +Shader "Builtin Gaussian Splat" +{ + Properties + { + _PointScale ("Point Scale", Float) = 1.0 + _OpacityScale ("Opacity Scale", Float) = 1.0 + } + HLSLINCLUDE + cbuffer PerObjectConstants + { + float4x4 gProjectionMatrix; + float4x4 gViewMatrix; + float4x4 gModelMatrix; + float4 gCameraRight; + float4 gCameraUp; + }; + + cbuffer MaterialConstants + { + float4 gSplatParams; + }; + + struct GaussianSplatOtherData + { + float4 rotation; + float4 scaleReserved; + }; + + StructuredBuffer GaussianSplatPositions; + StructuredBuffer GaussianSplatOther; + StructuredBuffer GaussianSplatColor; + + struct VSOutput + { + float4 position : SV_POSITION; + float2 localUv : TEXCOORD0; + float4 colorOpacity : TEXCOORD1; + }; + + float2 ResolveQuadCorner(uint vertexId) + { + switch (vertexId) + { + case 0u: return float2(-1.0, -1.0); + case 1u: return float2( 1.0, -1.0); + case 2u: return float2( 1.0, 1.0); + case 3u: return float2(-1.0, -1.0); + case 4u: return float2( 1.0, 1.0); + default: return float2(-1.0, 1.0); + } + } + + VSOutput MainVS(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID) + { + VSOutput output; + const float2 corner = ResolveQuadCorner(vertexId); + const float3 localCenter = GaussianSplatPositions[instanceId]; + const GaussianSplatOtherData otherData = GaussianSplatOther[instanceId]; + const float4 colorOpacity = GaussianSplatColor[instanceId]; + + const float3 worldCenter = mul(gModelMatrix, float4(localCenter, 1.0)).xyz; + const float maxAxisScale = + max(max(otherData.scaleReserved.x, otherData.scaleReserved.y), otherData.scaleReserved.z); + const float radius = max(maxAxisScale * gSplatParams.x, 0.0001); + const float3 worldPosition = + worldCenter + + gCameraRight.xyz * (corner.x * radius) + + gCameraUp.xyz * (corner.y * radius); + + output.position = mul(gProjectionMatrix, mul(gViewMatrix, float4(worldPosition, 1.0))); + output.localUv = corner; + output.colorOpacity = colorOpacity; + return output; + } + + float4 MainPS(VSOutput input) : SV_TARGET + { + const float radiusSq = dot(input.localUv, input.localUv); + if (radiusSq > 1.0) + { + discard; + } + + const float gaussianFalloff = exp(-radiusSq * 2.5); + const float alpha = saturate(input.colorOpacity.a * gSplatParams.y * gaussianFalloff); + if (alpha <= 0.001) + { + discard; + } + + return float4(input.colorOpacity.rgb, alpha); + } + ENDHLSL + + SubShader + { + Tags { "Queue" = "Transparent" } + Pass + { + Name "GaussianSplat" + Tags { "LightMode" = "GaussianSplat" } + Cull Off + ZWrite Off + ZTest LEqual + Blend SrcAlpha OneMinusSrcAlpha + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex MainVS + #pragma fragment MainPS + ENDHLSL + } + } +} diff --git a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h index 4d38225b..41c4bf5f 100644 --- a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h +++ b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h @@ -62,6 +62,18 @@ inline bool TryBuildBuiltinPassResourceBindingPlan( case BuiltinPassResourceSemantic::VolumeField: location = &outPlan.volumeField; break; + case BuiltinPassResourceSemantic::GaussianSplatPositionBuffer: + location = &outPlan.gaussianSplatPositionBuffer; + break; + case BuiltinPassResourceSemantic::GaussianSplatOtherBuffer: + location = &outPlan.gaussianSplatOtherBuffer; + break; + case BuiltinPassResourceSemantic::GaussianSplatColorBuffer: + location = &outPlan.gaussianSplatColorBuffer; + break; + case BuiltinPassResourceSemantic::GaussianSplatSHBuffer: + location = &outPlan.gaussianSplatSHBuffer; + break; case BuiltinPassResourceSemantic::BaseColorTexture: location = &outPlan.baseColorTexture; break; @@ -334,6 +346,24 @@ inline bool TryBuildBuiltinPassDefaultResourceBindings( return true; } + if (ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::GaussianSplat)) { + AppendBuiltinPassResourceBinding( + outBindings, + "PerObjectConstants", + Resources::ShaderResourceType::ConstantBuffer, + 0u, + 0u, + "PerObject"); + AppendBuiltinPassResourceBinding( + outBindings, + "MaterialConstants", + Resources::ShaderResourceType::ConstantBuffer, + 1u, + 0u, + "Material"); + return true; + } + if (ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::PostProcess)) { AppendBuiltinPassResourceBinding( outBindings, @@ -570,6 +600,18 @@ inline bool TryBuildBuiltinPassSetLayouts( case BuiltinPassResourceSemantic::VolumeField: setLayout.usesVolumeField = true; break; + case BuiltinPassResourceSemantic::GaussianSplatPositionBuffer: + setLayout.usesGaussianSplatPositionBuffer = true; + break; + case BuiltinPassResourceSemantic::GaussianSplatOtherBuffer: + setLayout.usesGaussianSplatOtherBuffer = true; + break; + case BuiltinPassResourceSemantic::GaussianSplatColorBuffer: + setLayout.usesGaussianSplatColorBuffer = true; + break; + case BuiltinPassResourceSemantic::GaussianSplatSHBuffer: + setLayout.usesGaussianSplatSHBuffer = true; + break; case BuiltinPassResourceSemantic::BaseColorTexture: setLayout.usesTexture = true; setLayout.usesBaseColorTexture = true; diff --git a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h index 711e6679..b99875be 100644 --- a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h +++ b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h @@ -27,6 +27,8 @@ inline const char* GetBuiltinPassCanonicalName(BuiltinMaterialPass pass) { return "selectionmask"; case BuiltinMaterialPass::Skybox: return "skybox"; + case BuiltinMaterialPass::GaussianSplat: + return "gaussiansplat"; case BuiltinMaterialPass::Volumetric: return "volumetric"; case BuiltinMaterialPass::PostProcess: @@ -145,6 +147,30 @@ inline BuiltinPassResourceSemantic ResolveBuiltinPassResourceSemantic( return BuiltinPassResourceSemantic::VolumeField; } + if (semantic == Containers::String("gaussiansplatpositionbuffer") || + semantic == Containers::String("gaussiansplatpositions") || + semantic == Containers::String("splatpositions")) { + return BuiltinPassResourceSemantic::GaussianSplatPositionBuffer; + } + + if (semantic == Containers::String("gaussiansplatotherbuffer") || + semantic == Containers::String("gaussiansplatother") || + semantic == Containers::String("splatother")) { + return BuiltinPassResourceSemantic::GaussianSplatOtherBuffer; + } + + if (semantic == Containers::String("gaussiansplatcolorbuffer") || + semantic == Containers::String("gaussiansplatcolor") || + semantic == Containers::String("splatcolor")) { + return BuiltinPassResourceSemantic::GaussianSplatColorBuffer; + } + + if (semantic == Containers::String("gaussiansplatshbuffer") || + semantic == Containers::String("gaussiansplatsh") || + semantic == Containers::String("splatsh")) { + return BuiltinPassResourceSemantic::GaussianSplatSHBuffer; + } + if (semantic == Containers::String("basecolortexture") || semantic == Containers::String("maintex")) { return BuiltinPassResourceSemantic::BaseColorTexture; @@ -207,6 +233,14 @@ inline const char* BuiltinPassResourceSemanticToString(BuiltinPassResourceSemant return "PassConstants"; case BuiltinPassResourceSemantic::VolumeField: return "VolumeField"; + case BuiltinPassResourceSemantic::GaussianSplatPositionBuffer: + return "GaussianSplatPositionBuffer"; + case BuiltinPassResourceSemantic::GaussianSplatOtherBuffer: + return "GaussianSplatOtherBuffer"; + case BuiltinPassResourceSemantic::GaussianSplatColorBuffer: + return "GaussianSplatColorBuffer"; + case BuiltinPassResourceSemantic::GaussianSplatSHBuffer: + return "GaussianSplatSHBuffer"; case BuiltinPassResourceSemantic::BaseColorTexture: return "BaseColorTexture"; case BuiltinPassResourceSemantic::SourceColorTexture: @@ -295,6 +329,12 @@ inline bool IsBuiltinPassResourceTypeCompatible( case BuiltinPassResourceSemantic::VolumeField: return type == Resources::ShaderResourceType::StructuredBuffer || type == Resources::ShaderResourceType::RawBuffer; + case BuiltinPassResourceSemantic::GaussianSplatPositionBuffer: + case BuiltinPassResourceSemantic::GaussianSplatOtherBuffer: + case BuiltinPassResourceSemantic::GaussianSplatColorBuffer: + case BuiltinPassResourceSemantic::GaussianSplatSHBuffer: + return type == Resources::ShaderResourceType::StructuredBuffer || + type == Resources::ShaderResourceType::RawBuffer; case BuiltinPassResourceSemantic::BaseColorTexture: case BuiltinPassResourceSemantic::SourceColorTexture: case BuiltinPassResourceSemantic::SkyboxPanoramicTexture: diff --git a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassTypes.h b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassTypes.h index 654f3404..f40bcde0 100644 --- a/engine/include/XCEngine/Rendering/Builtin/BuiltinPassTypes.h +++ b/engine/include/XCEngine/Rendering/Builtin/BuiltinPassTypes.h @@ -20,6 +20,7 @@ enum class BuiltinMaterialPass : Core::uint32 { ObjectId, SelectionMask, Skybox, + GaussianSplat, Volumetric, PostProcess, FinalColor, @@ -45,6 +46,11 @@ enum class BuiltinPassResourceSemantic : Core::uint8 { Environment, PassConstants, VolumeField, + GaussianSplatOrderBuffer, + GaussianSplatPositionBuffer, + GaussianSplatOtherBuffer, + GaussianSplatColorBuffer, + GaussianSplatSHBuffer, BaseColorTexture, SourceColorTexture, SkyboxPanoramicTexture, @@ -78,6 +84,11 @@ struct BuiltinPassResourceBindingPlan { PassResourceBindingLocation shadowReceiver = {}; PassResourceBindingLocation environment = {}; PassResourceBindingLocation passConstants = {}; + PassResourceBindingLocation gaussianSplatOrderBuffer = {}; + PassResourceBindingLocation gaussianSplatPositionBuffer = {}; + PassResourceBindingLocation gaussianSplatOtherBuffer = {}; + PassResourceBindingLocation gaussianSplatColorBuffer = {}; + PassResourceBindingLocation gaussianSplatSHBuffer = {}; PassResourceBindingLocation baseColorTexture = {}; PassResourceBindingLocation sourceColorTexture = {}; PassResourceBindingLocation skyboxPanoramicTexture = {}; @@ -111,6 +122,11 @@ struct BuiltinPassSetLayoutMetadata { bool usesPassConstants = false; bool usesMaterialBuffers = false; bool usesVolumeField = false; + bool usesGaussianSplatOrderBuffer = false; + bool usesGaussianSplatPositionBuffer = false; + bool usesGaussianSplatOtherBuffer = false; + bool usesGaussianSplatColorBuffer = false; + bool usesGaussianSplatSHBuffer = false; bool usesTexture = false; bool usesBaseColorTexture = false; bool usesSourceColorTexture = false; diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h new file mode 100644 index 00000000..ee0421cd --- /dev/null +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h @@ -0,0 +1,225 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Components { +class GameObject; +} // namespace Components + +namespace Resources { +class GaussianSplat; +class Material; +class Shader; +} // namespace Resources + +namespace Rendering { +struct VisibleGaussianSplatItem; + +namespace Passes { + +class BuiltinGaussianSplatPass final : public RenderPass { +public: + ~BuiltinGaussianSplatPass() override; + + const char* GetName() const override; + bool Initialize(const RenderContext& context) override; + bool PrepareGaussianSplatResources( + const RenderContext& context, + const RenderSceneData& sceneData); + bool Execute(const RenderPassContext& context) override; + void Shutdown() override; + +private: + struct PerObjectConstants { + Math::Matrix4x4 projection = Math::Matrix4x4::Identity(); + Math::Matrix4x4 view = Math::Matrix4x4::Identity(); + Math::Matrix4x4 model = Math::Matrix4x4::Identity(); + Math::Vector4 cameraRight = Math::Vector4::Zero(); + Math::Vector4 cameraUp = Math::Vector4::Zero(); + }; + + struct OwnedDescriptorSet { + RHI::RHIDescriptorPool* pool = nullptr; + RHI::RHIDescriptorSet* set = nullptr; + }; + + struct PassLayoutKey { + const Resources::Shader* shader = nullptr; + Containers::String passName; + + bool operator==(const PassLayoutKey& other) const { + return shader == other.shader && passName == other.passName; + } + }; + + struct PassLayoutKeyHash { + size_t operator()(const PassLayoutKey& key) const noexcept { + size_t hash = reinterpret_cast(key.shader); + hash ^= std::hash{}(key.passName) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + return hash; + } + }; + + struct PassResourceLayout { + RHI::RHIPipelineLayout* pipelineLayout = nullptr; + Core::uint32 firstDescriptorSet = 0; + Core::uint32 descriptorSetCount = 0; + std::vector setLayouts; + PassResourceBindingLocation perObject = {}; + PassResourceBindingLocation material = {}; + PassResourceBindingLocation gaussianSplatPositionBuffer = {}; + PassResourceBindingLocation gaussianSplatOtherBuffer = {}; + PassResourceBindingLocation gaussianSplatColorBuffer = {}; + PassResourceBindingLocation gaussianSplatSHBuffer = {}; + }; + + struct DynamicDescriptorSetKey { + PassLayoutKey passLayout = {}; + Core::uint32 setIndex = 0; + Core::uint64 objectId = 0; + const Resources::Material* material = nullptr; + const Resources::GaussianSplat* gaussianSplat = nullptr; + + bool operator==(const DynamicDescriptorSetKey& other) const { + return passLayout == other.passLayout && + setIndex == other.setIndex && + objectId == other.objectId && + material == other.material && + gaussianSplat == other.gaussianSplat; + } + }; + + struct DynamicDescriptorSetKeyHash { + size_t operator()(const DynamicDescriptorSetKey& key) const noexcept { + size_t hash = PassLayoutKeyHash()(key.passLayout); + hash ^= std::hash{}(key.setIndex) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.objectId) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= reinterpret_cast(key.material) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= reinterpret_cast(key.gaussianSplat) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + return hash; + } + }; + + struct CachedDescriptorSet { + OwnedDescriptorSet descriptorSet = {}; + Core::uint64 materialVersion = 0; + RHI::RHIResourceView* positionsView = nullptr; + RHI::RHIResourceView* otherView = nullptr; + RHI::RHIResourceView* colorView = nullptr; + RHI::RHIResourceView* shView = nullptr; + }; + + struct ResolvedShaderPass { + const Resources::Shader* shader = nullptr; + const Resources::ShaderPass* pass = nullptr; + Containers::String passName; + }; + + struct PipelineStateKey { + Resources::MaterialRenderState renderState; + const Resources::Shader* shader = nullptr; + Containers::String passName; + Containers::String keywordSignature; + Core::uint32 renderTargetCount = 0; + Core::uint32 renderTargetFormat = 0; + Core::uint32 depthStencilFormat = 0; + Core::uint32 sampleCount = 1; + Core::uint32 sampleQuality = 0; + + bool operator==(const PipelineStateKey& other) const { + return renderState == other.renderState && + shader == other.shader && + passName == other.passName && + keywordSignature == other.keywordSignature && + renderTargetCount == other.renderTargetCount && + renderTargetFormat == other.renderTargetFormat && + depthStencilFormat == other.depthStencilFormat && + sampleCount == other.sampleCount && + sampleQuality == other.sampleQuality; + } + }; + + struct PipelineStateKeyHash { + size_t operator()(const PipelineStateKey& key) const noexcept { + size_t hash = MaterialRenderStateHash()(key.renderState); + hash ^= reinterpret_cast(key.shader) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.passName) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.keywordSignature) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.renderTargetCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.renderTargetFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.depthStencilFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.sampleCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(key.sampleQuality) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + return hash; + } + }; + + bool EnsureInitialized(const RenderContext& context); + bool CreateResources(const RenderContext& context); + void DestroyResources(); + + const Resources::Material* ResolveGaussianSplatMaterial( + const VisibleGaussianSplatItem& visibleGaussianSplat) const; + ResolvedShaderPass ResolveGaussianSplatShaderPass( + const RenderSceneData& sceneData, + const Resources::Material* material) const; + PassResourceLayout* GetOrCreatePassResourceLayout( + const RenderContext& context, + const ResolvedShaderPass& resolvedShaderPass); + RHI::RHIPipelineState* GetOrCreatePipelineState( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData, + const Resources::Material* material); + bool CreateOwnedDescriptorSet( + const BuiltinPassSetLayoutMetadata& setLayout, + OwnedDescriptorSet& descriptorSet); + CachedDescriptorSet* GetOrCreateDynamicDescriptorSet( + const PassLayoutKey& passLayoutKey, + const PassResourceLayout& passLayout, + const BuiltinPassSetLayoutMetadata& setLayout, + Core::uint32 setIndex, + Core::uint64 objectId, + const Resources::Material* material, + const Resources::GaussianSplat* gaussianSplat, + const MaterialConstantPayloadView& materialConstants, + const RenderResourceCache::CachedGaussianSplat& cachedGaussianSplat); + void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet); + void DestroyPassResourceLayout(PassResourceLayout& passLayout); + bool DrawVisibleGaussianSplat( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData, + const VisibleGaussianSplatItem& visibleGaussianSplat); + + RHI::RHIDevice* m_device = nullptr; + RHI::RHIType m_backendType = RHI::RHIType::D3D12; + Resources::ResourceHandle m_builtinGaussianSplatShader; + std::unique_ptr m_builtinGaussianSplatMaterial; + RenderResourceCache m_resourceCache; + + std::unordered_map m_passResourceLayouts; + std::unordered_map m_pipelineStates; + std::unordered_map m_dynamicDescriptorSets; +}; + +} // namespace Passes +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h index a71d83a1..2c7a33bd 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h +++ b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h @@ -39,6 +39,7 @@ namespace Pipelines { } // namespace Pipelines namespace Passes { +class BuiltinGaussianSplatPass; class BuiltinVolumetricPass; } // namespace Passes @@ -355,6 +356,7 @@ private: OwnedDescriptorSet m_skyboxSamplerSet = {}; RHI::RHIResourceView* m_skyboxBoundPanoramicTextureView = nullptr; RHI::RHIResourceView* m_skyboxBoundCubemapTextureView = nullptr; + std::unique_ptr m_gaussianSplatPass; std::unique_ptr m_volumetricPass; }; diff --git a/engine/include/XCEngine/Resources/BuiltinResources.h b/engine/include/XCEngine/Resources/BuiltinResources.h index 55dbd1d4..1e1c6ed4 100644 --- a/engine/include/XCEngine/Resources/BuiltinResources.h +++ b/engine/include/XCEngine/Resources/BuiltinResources.h @@ -38,6 +38,7 @@ Containers::String GetBuiltinObjectIdOutlineShaderPath(); Containers::String GetBuiltinSelectionMaskShaderPath(); Containers::String GetBuiltinSelectionOutlineShaderPath(); Containers::String GetBuiltinSkyboxShaderPath(); +Containers::String GetBuiltinGaussianSplatShaderPath(); Containers::String GetBuiltinVolumetricShaderPath(); Containers::String GetBuiltinColorScalePostProcessShaderPath(); Containers::String GetBuiltinFinalColorShaderPath(); diff --git a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp new file mode 100644 index 00000000..529689b7 --- /dev/null +++ b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp @@ -0,0 +1,730 @@ +#include "Rendering/Passes/BuiltinGaussianSplatPass.h" + +#include "Components/GameObject.h" +#include "Debug/Logger.h" +#include "RHI/RHICommandList.h" +#include "RHI/RHIDevice.h" +#include "Rendering/Builtin/BuiltinPassLayoutUtils.h" +#include "Rendering/FrameData/RenderSceneData.h" +#include "Rendering/FrameData/VisibleGaussianSplatItem.h" +#include "Rendering/Internal/RenderSurfacePipelineUtils.h" +#include "Rendering/Internal/ShaderVariantUtils.h" +#include "Rendering/RenderSurface.h" +#include "Resources/BuiltinResources.h" +#include "Resources/GaussianSplat/GaussianSplat.h" +#include "Resources/Material/Material.h" +#include "Resources/Shader/Shader.h" + +namespace XCEngine { +namespace Rendering { +namespace Passes { + +namespace { + +Resources::ShaderKeywordSet ResolvePassKeywordSet( + const RenderSceneData& sceneData, + const Resources::Material* material) { + return Resources::CombineShaderKeywordSets( + sceneData.globalShaderKeywords, + material != nullptr ? material->GetKeywordSet() : Resources::ShaderKeywordSet()); +} + +const Resources::ShaderPass* FindCompatibleGaussianSplatPass( + const Resources::Shader& shader, + const RenderSceneData& sceneData, + const Resources::Material* material, + Resources::ShaderBackend backend) { + const Resources::ShaderKeywordSet keywordSet = ResolvePassKeywordSet(sceneData, material); + for (const Resources::ShaderPass& shaderPass : shader.GetPasses()) { + if (ShaderPassMatchesBuiltinPass(shaderPass, BuiltinMaterialPass::GaussianSplat) && + ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + shader, + shaderPass.name, + backend, + keywordSet)) { + return &shaderPass; + } + } + + return nullptr; +} + +RHI::GraphicsPipelineDesc CreatePipelineDesc( + RHI::RHIType backendType, + RHI::RHIPipelineLayout* pipelineLayout, + const Resources::Shader& shader, + const Resources::ShaderPass& shaderPass, + const Containers::String& passName, + const Resources::ShaderKeywordSet& keywordSet, + const Resources::Material* material, + const RenderSurface& surface) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + ::XCEngine::Rendering::Internal::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(surface, pipelineDesc); + pipelineDesc.depthStencilFormat = + static_cast(::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface)); + ApplyResolvedRenderState(&shaderPass, material, pipelineDesc); + + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Internal::ToShaderBackend(backendType); + if (const Resources::ShaderStageVariant* vertexVariant = + shader.FindVariant(passName, Resources::ShaderType::Vertex, backend, keywordSet)) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + shaderPass, + backend, + *vertexVariant, + pipelineDesc.vertexShader); + } + if (const Resources::ShaderStageVariant* fragmentVariant = + shader.FindVariant(passName, Resources::ShaderType::Fragment, backend, keywordSet)) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + shaderPass, + backend, + *fragmentVariant, + pipelineDesc.fragmentShader); + } + + return pipelineDesc; +} + +} // namespace + +BuiltinGaussianSplatPass::~BuiltinGaussianSplatPass() { + Shutdown(); +} + +const char* BuiltinGaussianSplatPass::GetName() const { + return "BuiltinGaussianSplatPass"; +} + +bool BuiltinGaussianSplatPass::Initialize(const RenderContext& context) { + return EnsureInitialized(context); +} + +bool BuiltinGaussianSplatPass::PrepareGaussianSplatResources( + const RenderContext& context, + const RenderSceneData& sceneData) { + if (!EnsureInitialized(context)) { + return false; + } + + for (const VisibleGaussianSplatItem& visibleGaussianSplat : sceneData.visibleGaussianSplats) { + if (visibleGaussianSplat.gaussianSplat == nullptr || + !visibleGaussianSplat.gaussianSplat->IsValid()) { + continue; + } + + const RenderResourceCache::CachedGaussianSplat* cachedGaussianSplat = + m_resourceCache.GetOrCreateGaussianSplat(m_device, visibleGaussianSplat.gaussianSplat); + if (cachedGaussianSplat == nullptr || + cachedGaussianSplat->positions.shaderResourceView == nullptr || + cachedGaussianSplat->other.shaderResourceView == nullptr || + cachedGaussianSplat->color.shaderResourceView == nullptr) { + return false; + } + } + + return true; +} + +bool BuiltinGaussianSplatPass::Execute(const RenderPassContext& context) { + if (!context.renderContext.IsValid()) { + return false; + } + + if (context.sceneData.visibleGaussianSplats.empty()) { + return true; + } + + const std::vector& colorAttachments = context.surface.GetColorAttachments(); + if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(context.surface) || + colorAttachments.empty() || + colorAttachments[0] == nullptr) { + return false; + } + + const Math::RectInt renderArea = context.surface.GetRenderArea(); + if (renderArea.width <= 0 || renderArea.height <= 0) { + return false; + } + + if (!PrepareGaussianSplatResources(context.renderContext, context.sceneData)) { + return false; + } + + RHI::RHICommandList* commandList = context.renderContext.commandList; + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + commandList->SetRenderTargets(1, &renderTarget, context.surface.GetDepthAttachment()); + + const RHI::Viewport viewport = { + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }; + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + + for (const VisibleGaussianSplatItem& visibleGaussianSplat : context.sceneData.visibleGaussianSplats) { + if (!DrawVisibleGaussianSplat( + context.renderContext, + context.surface, + context.sceneData, + visibleGaussianSplat)) { + return false; + } + } + + return true; +} + +void BuiltinGaussianSplatPass::Shutdown() { + DestroyResources(); +} + +bool BuiltinGaussianSplatPass::EnsureInitialized(const RenderContext& context) { + if (!context.IsValid()) { + return false; + } + + if (m_device == context.device && + m_backendType == context.backendType && + m_builtinGaussianSplatShader.IsValid() && + m_builtinGaussianSplatMaterial != nullptr) { + return true; + } + + DestroyResources(); + return CreateResources(context); +} + +bool BuiltinGaussianSplatPass::CreateResources(const RenderContext& context) { + m_device = context.device; + m_backendType = context.backendType; + m_builtinGaussianSplatShader = Resources::ResourceManager::Get().Load( + Resources::GetBuiltinGaussianSplatShaderPath()); + if (!m_builtinGaussianSplatShader.IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to load builtin gaussian splat shader resource"); + DestroyResources(); + return false; + } + + m_builtinGaussianSplatMaterial = std::make_unique(); + Resources::IResource::ConstructParams params = {}; + params.name = "BuiltinGaussianSplatMaterial"; + params.path = "builtin://materials/gaussian-splat-default"; + params.guid = Resources::ResourceGUID::Generate(params.path); + m_builtinGaussianSplatMaterial->Initialize(params); + m_builtinGaussianSplatMaterial->SetShader(m_builtinGaussianSplatShader); + m_builtinGaussianSplatMaterial->SetRenderQueue(Resources::MaterialRenderQueue::Transparent); + return true; +} + +void BuiltinGaussianSplatPass::DestroyResources() { + m_resourceCache.Shutdown(); + + for (auto& pipelinePair : m_pipelineStates) { + if (pipelinePair.second != nullptr) { + pipelinePair.second->Shutdown(); + delete pipelinePair.second; + } + } + m_pipelineStates.clear(); + + for (auto& descriptorSetPair : m_dynamicDescriptorSets) { + DestroyOwnedDescriptorSet(descriptorSetPair.second.descriptorSet); + } + m_dynamicDescriptorSets.clear(); + + for (auto& passLayoutPair : m_passResourceLayouts) { + DestroyPassResourceLayout(passLayoutPair.second); + } + m_passResourceLayouts.clear(); + + m_builtinGaussianSplatMaterial.reset(); + m_builtinGaussianSplatShader.Reset(); + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +const Resources::Material* BuiltinGaussianSplatPass::ResolveGaussianSplatMaterial( + const VisibleGaussianSplatItem& visibleGaussianSplat) const { + if (visibleGaussianSplat.material != nullptr && + visibleGaussianSplat.material->GetShader() != nullptr && + MatchesBuiltinPass(visibleGaussianSplat.material, BuiltinMaterialPass::GaussianSplat)) { + return visibleGaussianSplat.material; + } + + return m_builtinGaussianSplatMaterial.get(); +} + +BuiltinGaussianSplatPass::ResolvedShaderPass BuiltinGaussianSplatPass::ResolveGaussianSplatShaderPass( + const RenderSceneData& sceneData, + const Resources::Material* material) const { + ResolvedShaderPass resolved = {}; + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Internal::ToShaderBackend(m_backendType); + + if (material != nullptr && material->GetShader() != nullptr) { + const Resources::Shader* shader = material->GetShader(); + if (const Resources::ShaderPass* shaderPass = + FindCompatibleGaussianSplatPass(*shader, sceneData, material, backend)) { + resolved.shader = shader; + resolved.pass = shaderPass; + resolved.passName = shaderPass->name; + return resolved; + } + } + + if (m_builtinGaussianSplatShader.IsValid()) { + const Resources::Shader* shader = m_builtinGaussianSplatShader.Get(); + if (const Resources::ShaderPass* shaderPass = + FindCompatibleGaussianSplatPass(*shader, sceneData, material, backend)) { + resolved.shader = shader; + resolved.pass = shaderPass; + resolved.passName = shaderPass->name; + } + } + + return resolved; +} + +BuiltinGaussianSplatPass::PassResourceLayout* BuiltinGaussianSplatPass::GetOrCreatePassResourceLayout( + const RenderContext& context, + const ResolvedShaderPass& resolvedShaderPass) { + if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { + return nullptr; + } + + PassLayoutKey passLayoutKey = {}; + passLayoutKey.shader = resolvedShaderPass.shader; + passLayoutKey.passName = resolvedShaderPass.passName; + + const auto existing = m_passResourceLayouts.find(passLayoutKey); + if (existing != m_passResourceLayouts.end()) { + return &existing->second; + } + + PassResourceLayout passLayout = {}; + auto failLayout = [this, &passLayout](const char* message) -> PassResourceLayout* { + Debug::Logger::Get().Error(Debug::LogCategory::Rendering, message); + DestroyPassResourceLayout(passLayout); + return nullptr; + }; + + BuiltinPassResourceBindingPlan bindingPlan = {}; + Containers::String bindingPlanError; + if (!TryBuildBuiltinPassResourceBindingPlan(*resolvedShaderPass.pass, bindingPlan, &bindingPlanError)) { + const Containers::String contextualError = + Containers::String("BuiltinGaussianSplatPass failed to resolve pass resource bindings for shader='") + + resolvedShaderPass.shader->GetPath() + + "', pass='" + resolvedShaderPass.passName + + "': " + bindingPlanError + + ". Bindings: " + DescribeShaderResourceBindings(resolvedShaderPass.pass->resources); + return failLayout(contextualError.CStr()); + } + + Containers::String setLayoutError; + if (!TryBuildBuiltinPassSetLayouts(bindingPlan, passLayout.setLayouts, &setLayoutError)) { + return failLayout(setLayoutError.CStr()); + } + + passLayout.firstDescriptorSet = bindingPlan.firstDescriptorSet; + passLayout.descriptorSetCount = bindingPlan.descriptorSetCount; + passLayout.perObject = bindingPlan.perObject; + passLayout.material = bindingPlan.material; + passLayout.gaussianSplatPositionBuffer = bindingPlan.gaussianSplatPositionBuffer; + passLayout.gaussianSplatOtherBuffer = bindingPlan.gaussianSplatOtherBuffer; + passLayout.gaussianSplatColorBuffer = bindingPlan.gaussianSplatColorBuffer; + passLayout.gaussianSplatSHBuffer = bindingPlan.gaussianSplatSHBuffer; + + if (!passLayout.perObject.IsValid()) { + return failLayout("BuiltinGaussianSplatPass requires a PerObject resource binding"); + } + if (!passLayout.material.IsValid()) { + return failLayout("BuiltinGaussianSplatPass requires a Material resource binding"); + } + if (!passLayout.gaussianSplatPositionBuffer.IsValid() || + !passLayout.gaussianSplatOtherBuffer.IsValid() || + !passLayout.gaussianSplatColorBuffer.IsValid()) { + return failLayout("BuiltinGaussianSplatPass requires position, other, and color gaussian splat buffer bindings"); + } + + std::vector nativeSetLayouts(passLayout.setLayouts.size()); + for (size_t setIndex = 0; setIndex < passLayout.setLayouts.size(); ++setIndex) { + nativeSetLayouts[setIndex] = passLayout.setLayouts[setIndex].layout; + } + + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = nativeSetLayouts.empty() ? nullptr : nativeSetLayouts.data(); + pipelineLayoutDesc.setLayoutCount = static_cast(nativeSetLayouts.size()); + passLayout.pipelineLayout = context.device->CreatePipelineLayout(pipelineLayoutDesc); + if (passLayout.pipelineLayout == nullptr) { + return failLayout("BuiltinGaussianSplatPass failed to create pipeline layout from shader pass resources"); + } + + const auto result = m_passResourceLayouts.emplace(passLayoutKey, passLayout); + PassResourceLayout& storedPassLayout = result.first->second; + RefreshBuiltinPassSetLayouts(storedPassLayout.setLayouts); + return &storedPassLayout; +} + +RHI::RHIPipelineState* BuiltinGaussianSplatPass::GetOrCreatePipelineState( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData, + const Resources::Material* material) { + const Resources::ShaderKeywordSet keywordSet = ResolvePassKeywordSet(sceneData, material); + const ResolvedShaderPass resolvedShaderPass = ResolveGaussianSplatShaderPass(sceneData, material); + if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { + return nullptr; + } + + PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(context, resolvedShaderPass); + if (passLayout == nullptr || passLayout->pipelineLayout == nullptr) { + return nullptr; + } + + PipelineStateKey pipelineKey = {}; + pipelineKey.renderState = + BuildStaticPipelineRenderStateKey(ResolveEffectiveRenderState(resolvedShaderPass.pass, material)); + pipelineKey.shader = resolvedShaderPass.shader; + pipelineKey.passName = resolvedShaderPass.passName; + pipelineKey.keywordSignature = ::XCEngine::Rendering::Internal::BuildShaderKeywordSignature(keywordSet); + pipelineKey.renderTargetCount = + ::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) ? 1u : 0u; + pipelineKey.renderTargetFormat = + static_cast(::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat(surface, 0u)); + pipelineKey.depthStencilFormat = + static_cast(::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface)); + pipelineKey.sampleCount = ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface); + pipelineKey.sampleQuality = ::XCEngine::Rendering::Internal::ResolveSurfaceSampleQuality(surface); + + const auto existing = m_pipelineStates.find(pipelineKey); + if (existing != m_pipelineStates.end()) { + return existing->second; + } + + const RHI::GraphicsPipelineDesc pipelineDesc = + CreatePipelineDesc( + context.backendType, + passLayout->pipelineLayout, + *resolvedShaderPass.shader, + *resolvedShaderPass.pass, + resolvedShaderPass.passName, + keywordSet, + material, + surface); + RHI::RHIPipelineState* pipelineState = context.device->CreatePipelineState(pipelineDesc); + if (pipelineState == nullptr || !pipelineState->IsValid()) { + if (pipelineState != nullptr) { + pipelineState->Shutdown(); + delete pipelineState; + } + return nullptr; + } + + m_pipelineStates.emplace(pipelineKey, pipelineState); + return pipelineState; +} + +bool BuiltinGaussianSplatPass::CreateOwnedDescriptorSet( + const BuiltinPassSetLayoutMetadata& setLayout, + OwnedDescriptorSet& descriptorSet) { + RHI::DescriptorPoolDesc poolDesc = {}; + poolDesc.type = setLayout.heapType; + poolDesc.descriptorCount = CountBuiltinPassHeapDescriptors(setLayout.heapType, setLayout.bindings); + poolDesc.shaderVisible = setLayout.shaderVisible; + + descriptorSet.pool = m_device->CreateDescriptorPool(poolDesc); + if (descriptorSet.pool == nullptr) { + return false; + } + + descriptorSet.set = descriptorSet.pool->AllocateSet(setLayout.layout); + if (descriptorSet.set == nullptr) { + DestroyOwnedDescriptorSet(descriptorSet); + return false; + } + + return true; +} + +BuiltinGaussianSplatPass::CachedDescriptorSet* BuiltinGaussianSplatPass::GetOrCreateDynamicDescriptorSet( + const PassLayoutKey& passLayoutKey, + const PassResourceLayout& passLayout, + const BuiltinPassSetLayoutMetadata& setLayout, + Core::uint32 setIndex, + Core::uint64 objectId, + const Resources::Material* material, + const Resources::GaussianSplat* gaussianSplat, + const MaterialConstantPayloadView& materialConstants, + const RenderResourceCache::CachedGaussianSplat& cachedGaussianSplat) { + DynamicDescriptorSetKey key = {}; + key.passLayout = passLayoutKey; + key.setIndex = setIndex; + key.objectId = objectId; + key.material = material; + key.gaussianSplat = gaussianSplat; + + CachedDescriptorSet& cachedDescriptorSet = m_dynamicDescriptorSets[key]; + if (cachedDescriptorSet.descriptorSet.set == nullptr) { + if (!CreateOwnedDescriptorSet(setLayout, cachedDescriptorSet.descriptorSet)) { + return nullptr; + } + } + + const Core::uint64 materialVersion = material != nullptr ? material->GetChangeVersion() : 0u; + if (setLayout.usesMaterial) { + if (!passLayout.material.IsValid() || + passLayout.material.set != setIndex || + !materialConstants.IsValid()) { + return nullptr; + } + + if (cachedDescriptorSet.materialVersion != materialVersion) { + cachedDescriptorSet.descriptorSet.set->WriteConstant( + passLayout.material.binding, + materialConstants.data, + materialConstants.size); + } + } + + if (setLayout.usesGaussianSplatPositionBuffer) { + if (cachedGaussianSplat.positions.shaderResourceView == nullptr || + !passLayout.gaussianSplatPositionBuffer.IsValid() || + passLayout.gaussianSplatPositionBuffer.set != setIndex) { + return nullptr; + } + + if (cachedDescriptorSet.positionsView != cachedGaussianSplat.positions.shaderResourceView) { + cachedDescriptorSet.descriptorSet.set->Update( + passLayout.gaussianSplatPositionBuffer.binding, + cachedGaussianSplat.positions.shaderResourceView); + } + } + + if (setLayout.usesGaussianSplatOtherBuffer) { + if (cachedGaussianSplat.other.shaderResourceView == nullptr || + !passLayout.gaussianSplatOtherBuffer.IsValid() || + passLayout.gaussianSplatOtherBuffer.set != setIndex) { + return nullptr; + } + + if (cachedDescriptorSet.otherView != cachedGaussianSplat.other.shaderResourceView) { + cachedDescriptorSet.descriptorSet.set->Update( + passLayout.gaussianSplatOtherBuffer.binding, + cachedGaussianSplat.other.shaderResourceView); + } + } + + if (setLayout.usesGaussianSplatColorBuffer) { + if (cachedGaussianSplat.color.shaderResourceView == nullptr || + !passLayout.gaussianSplatColorBuffer.IsValid() || + passLayout.gaussianSplatColorBuffer.set != setIndex) { + return nullptr; + } + + if (cachedDescriptorSet.colorView != cachedGaussianSplat.color.shaderResourceView) { + cachedDescriptorSet.descriptorSet.set->Update( + passLayout.gaussianSplatColorBuffer.binding, + cachedGaussianSplat.color.shaderResourceView); + } + } + + if (setLayout.usesGaussianSplatSHBuffer) { + if (cachedGaussianSplat.sh.shaderResourceView == nullptr || + !passLayout.gaussianSplatSHBuffer.IsValid() || + passLayout.gaussianSplatSHBuffer.set != setIndex) { + return nullptr; + } + + if (cachedDescriptorSet.shView != cachedGaussianSplat.sh.shaderResourceView) { + cachedDescriptorSet.descriptorSet.set->Update( + passLayout.gaussianSplatSHBuffer.binding, + cachedGaussianSplat.sh.shaderResourceView); + } + } + + cachedDescriptorSet.materialVersion = materialVersion; + cachedDescriptorSet.positionsView = cachedGaussianSplat.positions.shaderResourceView; + cachedDescriptorSet.otherView = cachedGaussianSplat.other.shaderResourceView; + cachedDescriptorSet.colorView = cachedGaussianSplat.color.shaderResourceView; + cachedDescriptorSet.shView = cachedGaussianSplat.sh.shaderResourceView; + return &cachedDescriptorSet; +} + +void BuiltinGaussianSplatPass::DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet) { + if (descriptorSet.set != nullptr) { + descriptorSet.set->Shutdown(); + delete descriptorSet.set; + descriptorSet.set = nullptr; + } + + if (descriptorSet.pool != nullptr) { + descriptorSet.pool->Shutdown(); + delete descriptorSet.pool; + descriptorSet.pool = nullptr; + } +} + +void BuiltinGaussianSplatPass::DestroyPassResourceLayout(PassResourceLayout& passLayout) { + if (passLayout.pipelineLayout != nullptr) { + passLayout.pipelineLayout->Shutdown(); + delete passLayout.pipelineLayout; + passLayout.pipelineLayout = nullptr; + } + + passLayout.setLayouts.clear(); + passLayout.firstDescriptorSet = 0; + passLayout.descriptorSetCount = 0; + passLayout.perObject = {}; + passLayout.material = {}; + passLayout.gaussianSplatPositionBuffer = {}; + passLayout.gaussianSplatOtherBuffer = {}; + passLayout.gaussianSplatColorBuffer = {}; + passLayout.gaussianSplatSHBuffer = {}; +} + +bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData, + const VisibleGaussianSplatItem& visibleGaussianSplat) { + if (visibleGaussianSplat.gameObject == nullptr || + visibleGaussianSplat.gaussianSplat == nullptr || + !visibleGaussianSplat.gaussianSplat->IsValid()) { + return false; + } + + const RenderResourceCache::CachedGaussianSplat* cachedGaussianSplat = + m_resourceCache.GetOrCreateGaussianSplat(m_device, visibleGaussianSplat.gaussianSplat); + if (cachedGaussianSplat == nullptr || + cachedGaussianSplat->positions.shaderResourceView == nullptr || + cachedGaussianSplat->other.shaderResourceView == nullptr || + cachedGaussianSplat->color.shaderResourceView == nullptr) { + return false; + } + + const Resources::Material* material = ResolveGaussianSplatMaterial(visibleGaussianSplat); + if (material == nullptr) { + return false; + } + + const ResolvedShaderPass resolvedShaderPass = ResolveGaussianSplatShaderPass(sceneData, material); + if (resolvedShaderPass.shader == nullptr || resolvedShaderPass.pass == nullptr) { + return false; + } + + PassLayoutKey passLayoutKey = {}; + passLayoutKey.shader = resolvedShaderPass.shader; + passLayoutKey.passName = resolvedShaderPass.passName; + + PassResourceLayout* passLayout = GetOrCreatePassResourceLayout(context, resolvedShaderPass); + RHI::RHIPipelineState* pipelineState = GetOrCreatePipelineState(context, surface, sceneData, material); + if (passLayout == nullptr || pipelineState == nullptr) { + return false; + } + + const MaterialConstantPayloadView materialConstants = ResolveSchemaMaterialConstantPayload(material); + if (!materialConstants.IsValid()) { + return false; + } + + RHI::RHICommandList* commandList = context.commandList; + commandList->SetPipelineState(pipelineState); + + 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) + }; + + 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 false; + } + + const BuiltinPassSetLayoutMetadata& setLayout = passLayout->setLayouts[setIndex]; + if (!(setLayout.usesPerObject || + setLayout.usesMaterial || + setLayout.usesGaussianSplatPositionBuffer || + setLayout.usesGaussianSplatOtherBuffer || + setLayout.usesGaussianSplatColorBuffer || + setLayout.usesGaussianSplatSHBuffer)) { + return false; + } + + const Core::uint64 objectId = + setLayout.usesPerObject ? visibleGaussianSplat.gameObject->GetID() : 0u; + const Resources::Material* materialKey = + setLayout.usesMaterial ? material : nullptr; + const Resources::GaussianSplat* gaussianSplatKey = + (setLayout.usesGaussianSplatPositionBuffer || + setLayout.usesGaussianSplatOtherBuffer || + setLayout.usesGaussianSplatColorBuffer || + setLayout.usesGaussianSplatSHBuffer) + ? visibleGaussianSplat.gaussianSplat + : nullptr; + + CachedDescriptorSet* cachedDescriptorSet = GetOrCreateDynamicDescriptorSet( + passLayoutKey, + *passLayout, + setLayout, + setIndex, + objectId, + materialKey, + gaussianSplatKey, + materialConstants, + *cachedGaussianSplat); + if (cachedDescriptorSet == nullptr || cachedDescriptorSet->descriptorSet.set == nullptr) { + return false; + } + + RHI::RHIDescriptorSet* descriptorSet = cachedDescriptorSet->descriptorSet.set; + if (setLayout.usesPerObject) { + if (!passLayout->perObject.IsValid() || passLayout->perObject.set != setIndex) { + return false; + } + + descriptorSet->WriteConstant( + passLayout->perObject.binding, + &perObjectConstants, + sizeof(perObjectConstants)); + } + + descriptorSets[descriptorOffset] = descriptorSet; + } + + commandList->SetGraphicsDescriptorSets( + passLayout->firstDescriptorSet, + passLayout->descriptorSetCount, + descriptorSets.data(), + passLayout->pipelineLayout); + } + + ApplyDynamicRenderState(ResolveEffectiveRenderState(resolvedShaderPass.pass, material), *commandList); + commandList->Draw(6u, cachedGaussianSplat->splatCount, 0u, 0u); + return true; +} + +} // namespace Passes +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index 404b04c2..ffda2374 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -4,6 +4,7 @@ #include "Core/Asset/ResourceManager.h" #include "RHI/RHICommandList.h" #include "Rendering/Internal/RenderSurfacePipelineUtils.h" +#include "Rendering/Passes/BuiltinGaussianSplatPass.h" #include "Rendering/Materials/RenderMaterialResolve.h" #include "Rendering/Passes/BuiltinVolumetricPass.h" #include "Rendering/RenderSurface.h" @@ -66,6 +67,7 @@ std::vector CollectSurfaceColorAttachments(const RenderSu } // namespace BuiltinForwardPipeline::BuiltinForwardPipeline() { + m_gaussianSplatPass = std::make_unique(); m_volumetricPass = std::make_unique(); } @@ -109,11 +111,16 @@ RHI::InputLayoutDesc BuiltinForwardPipeline::BuildInputLayout() { bool BuiltinForwardPipeline::Initialize(const RenderContext& context) { return EnsureInitialized(context) && + m_gaussianSplatPass != nullptr && + m_gaussianSplatPass->Initialize(context) && m_volumetricPass != nullptr && m_volumetricPass->Initialize(context); } void BuiltinForwardPipeline::Shutdown() { + if (m_gaussianSplatPass != nullptr) { + m_gaussianSplatPass->Shutdown(); + } if (m_volumetricPass != nullptr) { m_volumetricPass->Shutdown(); } @@ -128,6 +135,17 @@ bool BuiltinForwardPipeline::Render( return false; } + if (m_volumetricPass != nullptr && + !sceneData.visibleVolumes.empty() && + !m_volumetricPass->PrepareVolumeResources(context, sceneData)) { + return false; + } + if (m_gaussianSplatPass != nullptr && + !sceneData.visibleGaussianSplats.empty() && + !m_gaussianSplatPass->PrepareGaussianSplatResources(context, sceneData)) { + return false; + } + const RenderPassContext passContext = { context, surface, @@ -150,6 +168,9 @@ bool BuiltinForwardPipeline::Render( if (renderResult) { renderResult = ExecuteForwardSkyboxPass(passContext); } + if (renderResult && m_gaussianSplatPass != nullptr) { + renderResult = m_gaussianSplatPass->Execute(passContext); + } if (renderResult && m_volumetricPass != nullptr) { renderResult = m_volumetricPass->Execute(passContext); } diff --git a/engine/src/Resources/BuiltinResources.cpp b/engine/src/Resources/BuiltinResources.cpp index 92301be9..73ce3106 100644 --- a/engine/src/Resources/BuiltinResources.cpp +++ b/engine/src/Resources/BuiltinResources.cpp @@ -36,6 +36,7 @@ constexpr const char* kBuiltinObjectIdOutlineShaderPath = "builtin://shaders/obj constexpr const char* kBuiltinSelectionMaskShaderPath = "builtin://shaders/selection-mask"; constexpr const char* kBuiltinSelectionOutlineShaderPath = "builtin://shaders/selection-outline"; constexpr const char* kBuiltinSkyboxShaderPath = "builtin://shaders/skybox"; +constexpr const char* kBuiltinGaussianSplatShaderPath = "builtin://shaders/gaussian-splat"; constexpr const char* kBuiltinVolumetricShaderPath = "builtin://shaders/volumetric"; constexpr const char* kBuiltinColorScalePostProcessShaderPath = "builtin://shaders/color-scale-post-process"; @@ -68,6 +69,8 @@ constexpr const char* kBuiltinSelectionOutlineShaderAssetRelativePath = "engine/assets/builtin/shaders/selection-outline.shader"; constexpr const char* kBuiltinSkyboxShaderAssetRelativePath = "engine/assets/builtin/shaders/skybox.shader"; +constexpr const char* kBuiltinGaussianSplatShaderAssetRelativePath = + "engine/assets/builtin/shaders/gaussian-splat.shader"; constexpr const char* kBuiltinVolumetricShaderAssetRelativePath = "engine/assets/builtin/shaders/volumetric.shader"; constexpr const char* kBuiltinColorScalePostProcessShaderAssetRelativePath = @@ -164,6 +167,9 @@ const char* GetBuiltinShaderAssetRelativePath(const Containers::String& builtinS if (builtinShaderPath == Containers::String(kBuiltinSkyboxShaderPath)) { return kBuiltinSkyboxShaderAssetRelativePath; } + if (builtinShaderPath == Containers::String(kBuiltinGaussianSplatShaderPath)) { + return kBuiltinGaussianSplatShaderAssetRelativePath; + } if (builtinShaderPath == Containers::String(kBuiltinVolumetricShaderPath)) { return kBuiltinVolumetricShaderAssetRelativePath; } @@ -734,6 +740,10 @@ Shader* BuildBuiltinSkyboxShader(const Containers::String& path) { return TryLoadBuiltinShaderFromAsset(path); } +Shader* BuildBuiltinGaussianSplatShader(const Containers::String& path) { + return TryLoadBuiltinShaderFromAsset(path); +} + Shader* BuildBuiltinVolumetricShader(const Containers::String& path) { return TryLoadBuiltinShaderFromAsset(path); } @@ -852,6 +862,10 @@ bool TryGetBuiltinShaderPathByShaderName( outPath = GetBuiltinSkyboxShaderPath(); return true; } + if (shaderName == "Builtin Gaussian Splat") { + outPath = GetBuiltinGaussianSplatShaderPath(); + return true; + } if (shaderName == "Builtin Volumetric") { outPath = GetBuiltinVolumetricShaderPath(); return true; @@ -939,6 +953,10 @@ Containers::String GetBuiltinSkyboxShaderPath() { return Containers::String(kBuiltinSkyboxShaderPath); } +Containers::String GetBuiltinGaussianSplatShaderPath() { + return Containers::String(kBuiltinGaussianSplatShaderPath); +} + Containers::String GetBuiltinVolumetricShaderPath() { return Containers::String(kBuiltinVolumetricShaderPath); } @@ -1057,6 +1075,8 @@ LoadResult CreateBuiltinShaderResource(const Containers::String& path) { shader = BuildBuiltinSelectionOutlineShader(path); } else if (path == GetBuiltinSkyboxShaderPath()) { shader = BuildBuiltinSkyboxShader(path); + } else if (path == GetBuiltinGaussianSplatShaderPath()) { + shader = BuildBuiltinGaussianSplatShader(path); } else if (path == GetBuiltinVolumetricShaderPath()) { shader = BuildBuiltinVolumetricShader(path); } else if (path == GetBuiltinColorScalePostProcessShaderPath()) { diff --git a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp index a8cf23d9..d538ff0b 100644 --- a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp +++ b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp @@ -1,12 +1,14 @@ #include -#include "Rendering/Detail/ShaderVariantUtils.h" +#include "Rendering/Internal/ShaderVariantUtils.h" #include +#include "Rendering/Internal/RenderSurfacePipelineUtils.h" #include #include #include #include +#include #include #include #include @@ -87,6 +89,46 @@ void AppendDefaultBuiltinPassResources(ShaderPass& pass) { } } +class TestResourceView final : public XCEngine::RHI::RHIResourceView { +public: + TestResourceView( + XCEngine::RHI::ResourceViewType viewType, + XCEngine::RHI::ResourceViewDimension dimension, + XCEngine::RHI::Format format) + : m_viewType(viewType), + m_dimension(dimension), + m_format(format) { + } + + void Shutdown() override { + } + + void* GetNativeHandle() override { + return nullptr; + } + + bool IsValid() const override { + return true; + } + + XCEngine::RHI::ResourceViewType GetViewType() const override { + return m_viewType; + } + + XCEngine::RHI::ResourceViewDimension GetDimension() const override { + return m_dimension; + } + + XCEngine::RHI::Format GetFormat() const override { + return m_format; + } + +private: + XCEngine::RHI::ResourceViewType m_viewType; + XCEngine::RHI::ResourceViewDimension m_dimension; + XCEngine::RHI::Format m_format; +}; + } // namespace TEST(BuiltinForwardPipeline_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) { @@ -116,6 +158,162 @@ TEST(BuiltinForwardPipeline_Test, UsesFloat3PositionInputLayoutForStaticMeshVert EXPECT_EQ(texcoord.alignedByteOffset, static_cast(offsetof(StaticMeshVertex, uv0))); } +TEST(RenderSurfacePipelineUtils_Test, ResolvesContiguousSurfaceAttachmentFormatsIntoPipelineDesc) { + TestResourceView color0( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R16G16B16A16_Float); + TestResourceView color1( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R8G8B8A8_UNorm); + TestResourceView depth( + ResourceViewType::DepthStencil, + ResourceViewDimension::Texture2D, + Format::D32_Float); + + RenderSurface surface(1280u, 720u); + surface.SetColorAttachments({ &color0, &color1 }); + surface.SetDepthAttachment(&depth); + surface.SetSampleDesc(4u, 0u); + + GraphicsPipelineDesc pipelineDesc = {}; + XCEngine::Rendering::Internal::ApplySurfacePropertiesToGraphicsPipelineDesc(surface, pipelineDesc); + + EXPECT_EQ(pipelineDesc.renderTargetCount, 2u); + EXPECT_EQ(static_cast(pipelineDesc.renderTargetFormats[0]), Format::R16G16B16A16_Float); + EXPECT_EQ(static_cast(pipelineDesc.renderTargetFormats[1]), Format::R8G8B8A8_UNorm); + EXPECT_EQ(static_cast(pipelineDesc.depthStencilFormat), Format::D32_Float); + EXPECT_EQ(pipelineDesc.sampleCount, 4u); + EXPECT_EQ(pipelineDesc.sampleQuality, 0u); +} + +TEST(RenderSurfacePipelineUtils_Test, PropagatesSampleQualityIntoPipelineDesc) { + TestResourceView color0( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R8G8B8A8_UNorm); + + RenderSurface surface(640u, 360u); + surface.SetColorAttachment(&color0); + surface.SetSampleDesc(4u, 3u); + + GraphicsPipelineDesc pipelineDesc = {}; + XCEngine::Rendering::Internal::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(surface, pipelineDesc); + + EXPECT_EQ(pipelineDesc.renderTargetCount, 1u); + EXPECT_EQ(pipelineDesc.sampleCount, 4u); + EXPECT_EQ(pipelineDesc.sampleQuality, 3u); +} + +TEST(RenderSurfacePipelineUtils_Test, StopsAtFirstNullColorAttachmentWhenResolvingFormats) { + TestResourceView color0( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R16G16B16A16_Float); + TestResourceView color2( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R8G8B8A8_UNorm); + + RenderSurface surface(1280u, 720u); + surface.SetColorAttachments({ &color0, nullptr, &color2 }); + + const uint32_t colorAttachmentCount = + XCEngine::Rendering::Internal::ResolveSurfaceColorAttachmentCount(surface); + const auto colorFormats = + XCEngine::Rendering::Internal::ResolveSurfaceColorFormats(surface); + + EXPECT_EQ(colorAttachmentCount, 1u); + EXPECT_EQ(static_cast(colorFormats[0]), Format::R16G16B16A16_Float); + EXPECT_EQ(static_cast(colorFormats[1]), Format::Unknown); +} + +TEST(RenderSurfacePipelineUtils_Test, NormalizesInvalidSampleCountToSingleSample) { + RenderSurface surface(640u, 480u); + surface.SetSampleDesc(0u, 7u); + + EXPECT_EQ(surface.GetSampleCount(), 1u); + EXPECT_EQ(surface.GetSampleQuality(), 0u); + EXPECT_EQ(XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface), 1u); +} + +TEST(RenderSurfacePipelineUtils_Test, AcceptsDepthStyleSurfaceWithDepthOnlyOutput) { + TestResourceView depth( + ResourceViewType::DepthStencil, + ResourceViewDimension::Texture2D, + Format::D32_Float); + + RenderSurface surface(640u, 480u); + surface.SetDepthAttachment(&depth); + surface.SetSampleDesc(1u, 0u); + + EXPECT_TRUE(XCEngine::Rendering::Internal::IsDepthStyleCompatibleSurface(surface)); +} + +TEST(RenderSurfacePipelineUtils_Test, RejectsDepthStyleSurfaceWithoutKnownDepthFormat) { + TestResourceView color( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R8G8B8A8_UNorm); + + RenderSurface surface(640u, 480u); + surface.SetColorAttachment(&color); + + EXPECT_FALSE(XCEngine::Rendering::Internal::IsDepthStyleCompatibleSurface(surface)); +} + +TEST(RenderSurfacePipelineUtils_Test, RejectsDepthStyleSurfaceWithUnknownSingleColorFormat) { + TestResourceView color( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::Unknown); + TestResourceView depth( + ResourceViewType::DepthStencil, + ResourceViewDimension::Texture2D, + Format::D32_Float); + + RenderSurface surface(640u, 480u); + surface.SetColorAttachment(&color); + surface.SetDepthAttachment(&depth); + + EXPECT_FALSE(XCEngine::Rendering::Internal::IsDepthStyleCompatibleSurface(surface)); +} + +TEST(RenderSurfacePipelineUtils_Test, RejectsDepthStyleSurfaceWithMultipleColorAttachments) { + TestResourceView color0( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R16G16B16A16_Float); + TestResourceView color1( + ResourceViewType::RenderTarget, + ResourceViewDimension::Texture2D, + Format::R8G8B8A8_UNorm); + TestResourceView depth( + ResourceViewType::DepthStencil, + ResourceViewDimension::Texture2D, + Format::D32_Float); + + RenderSurface surface(640u, 480u); + surface.SetColorAttachments({ &color0, &color1 }); + surface.SetDepthAttachment(&depth); + + EXPECT_FALSE(XCEngine::Rendering::Internal::IsDepthStyleCompatibleSurface(surface)); +} + +TEST(RenderSurface_Test, DefaultsDepthStateToDepthWriteAndSupportsOverrides) { + RenderSurface surface(640u, 480u); + + EXPECT_EQ(surface.GetDepthStateBefore(), ResourceStates::DepthWrite); + EXPECT_EQ(surface.GetDepthStateAfter(), ResourceStates::DepthWrite); + + surface.SetDepthStateBefore(ResourceStates::Common); + surface.SetDepthStateAfter(ResourceStates::PixelShaderResource); + + EXPECT_EQ(surface.GetDepthStateBefore(), ResourceStates::Common); + EXPECT_EQ(surface.GetDepthStateAfter(), ResourceStates::PixelShaderResource); +} + TEST(BuiltinForwardPipeline_Test, SplitsSceneItemsIntoOpaqueAndTransparentQueueRanges) { EXPECT_FALSE(IsTransparentRenderQueue(static_cast(MaterialRenderQueue::Background))); EXPECT_FALSE(IsTransparentRenderQueue(static_cast(MaterialRenderQueue::Geometry))); @@ -207,7 +405,7 @@ TEST(BuiltinForwardPipeline_Test, BuiltinUnlitShaderBuildsVulkanRuntimeSourceWit ASSERT_NE(fragmentVariant, nullptr); const std::string runtimeSource = - ::XCEngine::Rendering::Detail::BuildRuntimeShaderSource( + ::XCEngine::Rendering::Internal::BuildRuntimeShaderSource( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *fragmentVariant); @@ -243,7 +441,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringUnlit ASSERT_NE(d3d12Fragment, nullptr); ShaderCompileDesc d3d12CompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::D3D12, *d3d12Fragment, @@ -271,7 +469,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringUnlit ASSERT_NE(vulkanFragment, nullptr); ShaderCompileDesc vulkanCompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *vulkanFragment, @@ -314,7 +512,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringForwa ASSERT_NE(d3d12Fragment, nullptr); ShaderCompileDesc d3d12CompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::D3D12, *d3d12Fragment, @@ -365,7 +563,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringForwa ASSERT_NE(vulkanFragment, nullptr); ShaderCompileDesc vulkanCompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *vulkanFragment, @@ -432,7 +630,7 @@ TEST(BuiltinForwardPipeline_Test, OpenGLRuntimeTranspilesForwardShadowVariantToL ASSERT_NE(openGLFragment, nullptr); ShaderCompileDesc compileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::OpenGL, *openGLFragment, @@ -509,7 +707,7 @@ TEST(BuiltinDepthStylePass_Test, OpenGLRuntimeTranspilesDepthOnlyAlphaVariantWit const ShaderStageVariant& variant, std::string& glslSource) { ShaderCompileDesc compileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( targetPass, XCEngine::Resources::ShaderBackend::OpenGL, variant, @@ -573,7 +771,7 @@ TEST(BuiltinDepthStylePass_Test, OpenGLRuntimeTranspilesShadowCasterAlphaVariant const ShaderStageVariant& variant, std::string& glslSource) { ShaderCompileDesc compileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( targetPass, XCEngine::Resources::ShaderBackend::OpenGL, variant, @@ -676,6 +874,219 @@ TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoaded delete shader; } +TEST(BuiltinForwardPipeline_Test, BuiltinGaussianSplatShaderUsesAuthoringContract) { + ShaderLoader loader; + LoadResult result = loader.Load(GetBuiltinGaussianSplatShaderPath()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderPass* pass = shader->FindPass("GaussianSplat"); + ASSERT_NE(pass, nullptr); + EXPECT_EQ(pass->resources.Size(), 5u); + EXPECT_TRUE(pass->hasFixedFunctionState); + EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None); + EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable); + EXPECT_EQ(pass->fixedFunctionState.depthFunc, MaterialComparisonFunc::LessEqual); + EXPECT_TRUE(pass->fixedFunctionState.blendEnable); + + const ShaderPropertyDesc* pointScale = shader->FindProperty("_PointScale"); + ASSERT_NE(pointScale, nullptr); + EXPECT_EQ(pointScale->type, ShaderPropertyType::Float); + + const ShaderPropertyDesc* opacityScale = shader->FindProperty("_OpacityScale"); + ASSERT_NE(opacityScale, nullptr); + EXPECT_EQ(opacityScale->type, ShaderPropertyType::Float); + + const ShaderResourceBindingDesc* positions = + shader->FindPassResourceBinding("GaussianSplat", "GaussianSplatPositions"); + ASSERT_NE(positions, nullptr); + EXPECT_EQ(positions->type, ShaderResourceType::StructuredBuffer); + EXPECT_EQ(positions->set, 2u); + EXPECT_EQ(positions->binding, 0u); + EXPECT_EQ( + ResolveBuiltinPassResourceSemantic(*positions), + BuiltinPassResourceSemantic::GaussianSplatPositionBuffer); + + const ShaderResourceBindingDesc* other = + shader->FindPassResourceBinding("GaussianSplat", "GaussianSplatOther"); + ASSERT_NE(other, nullptr); + EXPECT_EQ(other->type, ShaderResourceType::StructuredBuffer); + EXPECT_EQ(other->set, 2u); + EXPECT_EQ(other->binding, 1u); + EXPECT_EQ( + ResolveBuiltinPassResourceSemantic(*other), + BuiltinPassResourceSemantic::GaussianSplatOtherBuffer); + + const ShaderResourceBindingDesc* color = + shader->FindPassResourceBinding("GaussianSplat", "GaussianSplatColor"); + ASSERT_NE(color, nullptr); + EXPECT_EQ(color->type, ShaderResourceType::StructuredBuffer); + EXPECT_EQ(color->set, 2u); + EXPECT_EQ(color->binding, 2u); + EXPECT_EQ( + ResolveBuiltinPassResourceSemantic(*color), + BuiltinPassResourceSemantic::GaussianSplatColorBuffer); + + delete shader; +} + +TEST(BuiltinForwardPipeline_Test, BuildsBuiltinPassResourceBindingPlanFromLoadedGaussianSplatShaderContract) { + ShaderLoader loader; + LoadResult result = loader.Load(GetBuiltinGaussianSplatShaderPath()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderPass* pass = shader->FindPass("GaussianSplat"); + ASSERT_NE(pass, nullptr); + + BuiltinPassResourceBindingPlan plan = {}; + String error; + EXPECT_TRUE(TryBuildBuiltinPassResourceBindingPlan(*pass, plan, &error)) << error.CStr(); + ASSERT_EQ(plan.bindings.Size(), 5u); + EXPECT_TRUE(plan.perObject.IsValid()); + EXPECT_TRUE(plan.material.IsValid()); + EXPECT_TRUE(plan.gaussianSplatPositionBuffer.IsValid()); + EXPECT_TRUE(plan.gaussianSplatOtherBuffer.IsValid()); + EXPECT_TRUE(plan.gaussianSplatColorBuffer.IsValid()); + EXPECT_FALSE(plan.gaussianSplatSHBuffer.IsValid()); + EXPECT_EQ(plan.perObject.set, 0u); + EXPECT_EQ(plan.material.set, 1u); + EXPECT_EQ(plan.gaussianSplatPositionBuffer.set, 2u); + EXPECT_EQ(plan.gaussianSplatPositionBuffer.binding, 0u); + EXPECT_EQ(plan.gaussianSplatOtherBuffer.set, 2u); + EXPECT_EQ(plan.gaussianSplatOtherBuffer.binding, 1u); + EXPECT_EQ(plan.gaussianSplatColorBuffer.set, 2u); + EXPECT_EQ(plan.gaussianSplatColorBuffer.binding, 2u); + EXPECT_EQ(plan.firstDescriptorSet, 0u); + EXPECT_EQ(plan.descriptorSetCount, 3u); + + std::vector setLayouts; + ASSERT_TRUE(TryBuildBuiltinPassSetLayouts(plan, setLayouts, &error)) << error.CStr(); + ASSERT_EQ(setLayouts.size(), 3u); + EXPECT_TRUE(setLayouts[0].usesPerObject); + EXPECT_TRUE(setLayouts[1].usesMaterial); + EXPECT_TRUE(setLayouts[2].usesGaussianSplatPositionBuffer); + EXPECT_TRUE(setLayouts[2].usesGaussianSplatOtherBuffer); + EXPECT_TRUE(setLayouts[2].usesGaussianSplatColorBuffer); + ASSERT_EQ(setLayouts[2].bindings.size(), 3u); + EXPECT_EQ( + static_cast(setLayouts[2].bindings[0].type), + DescriptorType::SRV); + EXPECT_EQ( + static_cast(setLayouts[2].bindings[1].type), + DescriptorType::SRV); + EXPECT_EQ( + static_cast(setLayouts[2].bindings[2].type), + DescriptorType::SRV); + EXPECT_EQ( + setLayouts[2].bindings[0].resourceDimension, + ResourceViewDimension::StructuredBuffer); + EXPECT_EQ( + setLayouts[2].bindings[1].resourceDimension, + ResourceViewDimension::StructuredBuffer); + EXPECT_EQ( + setLayouts[2].bindings[2].resourceDimension, + ResourceViewDimension::StructuredBuffer); + + delete shader; +} + +TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringGaussianSplatBindingsToDescriptorSpaces) { + ShaderLoader loader; + LoadResult result = loader.Load(GetBuiltinGaussianSplatShaderPath()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Shader* shader = static_cast(result.resource); + ASSERT_NE(shader, nullptr); + + const ShaderPass* pass = shader->FindPass("GaussianSplat"); + ASSERT_NE(pass, nullptr); + + const ShaderStageVariant* d3d12Vertex = shader->FindVariant( + "GaussianSplat", + XCEngine::Resources::ShaderType::Vertex, + XCEngine::Resources::ShaderBackend::D3D12); + ASSERT_NE(d3d12Vertex, nullptr); + + ShaderCompileDesc d3d12CompileDesc = {}; + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + *pass, + XCEngine::Resources::ShaderBackend::D3D12, + *d3d12Vertex, + d3d12CompileDesc); + const std::string d3d12Source( + reinterpret_cast(d3d12CompileDesc.source.data()), + d3d12CompileDesc.source.size()); + EXPECT_TRUE(SourceContainsRegisterBinding( + d3d12Source, + "cbuffer PerObjectConstants", + "register(b0)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + d3d12Source, + "cbuffer MaterialConstants", + "register(b1)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + d3d12Source, + "StructuredBuffer GaussianSplatPositions", + "register(t0)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + d3d12Source, + "StructuredBuffer GaussianSplatOther", + "register(t1)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + d3d12Source, + "StructuredBuffer GaussianSplatColor", + "register(t2)")); + EXPECT_EQ(d3d12Source.find("space0"), std::string::npos); + EXPECT_EQ(d3d12Source.find("space1"), std::string::npos); + EXPECT_EQ(d3d12Source.find("space2"), std::string::npos); + + const ShaderStageVariant* vulkanVertex = shader->FindVariant( + "GaussianSplat", + XCEngine::Resources::ShaderType::Vertex, + XCEngine::Resources::ShaderBackend::Vulkan); + ASSERT_NE(vulkanVertex, nullptr); + + ShaderCompileDesc vulkanCompileDesc = {}; + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + *pass, + XCEngine::Resources::ShaderBackend::Vulkan, + *vulkanVertex, + vulkanCompileDesc); + const std::string vulkanSource( + reinterpret_cast(vulkanCompileDesc.source.data()), + vulkanCompileDesc.source.size()); + EXPECT_TRUE(SourceContainsRegisterBinding( + vulkanSource, + "cbuffer PerObjectConstants", + "register(b0, space0)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + vulkanSource, + "cbuffer MaterialConstants", + "register(b0, space1)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + vulkanSource, + "StructuredBuffer GaussianSplatPositions", + "register(t0, space2)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + vulkanSource, + "StructuredBuffer GaussianSplatOther", + "register(t1, space2)")); + EXPECT_TRUE(SourceContainsRegisterBinding( + vulkanSource, + "StructuredBuffer GaussianSplatColor", + "register(t2, space2)")); + + delete shader; +} + TEST(BuiltinForwardPipeline_Test, BuiltinVolumetricShaderUsesAuthoringContract) { ShaderLoader loader; LoadResult result = loader.Load(GetBuiltinVolumetricShaderPath()); @@ -893,7 +1304,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringFinal ASSERT_NE(d3d12Fragment, nullptr); ShaderCompileDesc d3d12CompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::D3D12, *d3d12Fragment, @@ -923,7 +1334,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringFinal ASSERT_NE(vulkanFragment, nullptr); const std::string runtimeSource = - ::XCEngine::Rendering::Detail::BuildRuntimeShaderSource( + ::XCEngine::Rendering::Internal::BuildRuntimeShaderSource( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *vulkanFragment); @@ -941,7 +1352,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringFinal "register(s0, space2)")); ShaderCompileDesc vulkanCompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *vulkanFragment, @@ -974,7 +1385,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringVolum ASSERT_NE(d3d12Fragment, nullptr); ShaderCompileDesc d3d12CompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::D3D12, *d3d12Fragment, @@ -1006,7 +1417,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringVolum ASSERT_NE(vulkanFragment, nullptr); ShaderCompileDesc vulkanCompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *vulkanFragment, @@ -1053,7 +1464,7 @@ TEST(BuiltinForwardPipeline_Test, OpenGLRuntimeTranspilesFinalColorVariantToComb ASSERT_NE(openGLFragment, nullptr); ShaderCompileDesc compileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::OpenGL, *openGLFragment, @@ -1098,7 +1509,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringColor ASSERT_NE(d3d12Fragment, nullptr); ShaderCompileDesc d3d12CompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::D3D12, *d3d12Fragment, @@ -1128,7 +1539,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringColor ASSERT_NE(vulkanFragment, nullptr); const std::string runtimeSource = - ::XCEngine::Rendering::Detail::BuildRuntimeShaderSource( + ::XCEngine::Rendering::Internal::BuildRuntimeShaderSource( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *vulkanFragment); @@ -1146,7 +1557,7 @@ TEST(BuiltinForwardPipeline_Test, VulkanRuntimeCompileDescRewritesAuthoringColor "register(s0, space2)")); ShaderCompileDesc vulkanCompileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::Vulkan, *vulkanFragment, @@ -1179,7 +1590,7 @@ TEST(BuiltinForwardPipeline_Test, OpenGLRuntimeTranspilesColorScaleVariantToComb ASSERT_NE(openGLFragment, nullptr); ShaderCompileDesc compileDesc = {}; - ::XCEngine::Rendering::Detail::ApplyShaderStageVariant( + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( *pass, XCEngine::Resources::ShaderBackend::OpenGL, *openGLFragment,