From 15b42c248f3bd95d73226f3383e524537b2ee6a5 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 10 Apr 2026 22:15:05 +0800 Subject: [PATCH] Formalize GaussianSplat transient pass resources --- engine/CMakeLists.txt | 1 + .../BuiltinGaussianSplatPassResources.cpp | 297 ++++++++++++ .../BuiltinGaussianSplatPassResources.h | 104 +++++ tests/Rendering/unit/CMakeLists.txt | 1 + ..._builtin_gaussian_splat_pass_resources.cpp | 441 ++++++++++++++++++ 5 files changed, 844 insertions(+) create mode 100644 engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp create mode 100644 engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h create mode 100644 tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index b7510dcd..71ce84ce 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -526,6 +526,7 @@ add_library(XCEngine STATIC ${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/BuiltinVolumetricPass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Extraction/RenderSceneExtractor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Extraction/RenderSceneUtility.cpp diff --git a/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp new file mode 100644 index 00000000..a0ad202a --- /dev/null +++ b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.cpp @@ -0,0 +1,297 @@ +#include "Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h" + +#include "Components/GaussianSplatRendererComponent.h" +#include "Resources/GaussianSplat/GaussianSplat.h" + +namespace XCEngine { +namespace Rendering { +namespace Passes { +namespace Internal { + +namespace { + +bool CreateStructuredBufferViews( + RHI::RHIDevice* device, + Core::uint32 elementCount, + Core::uint32 elementStride, + BuiltinGaussianSplatPassResources::CachedBufferView& bufferView) { + if (device == nullptr || elementCount == 0u || elementStride == 0u) { + return false; + } + + RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = static_cast(elementCount) * static_cast(elementStride); + bufferDesc.stride = elementStride; + bufferDesc.bufferType = static_cast(RHI::BufferType::Storage); + bufferDesc.flags = 0u; + + bufferView.buffer = device->CreateBuffer(bufferDesc); + if (bufferView.buffer == nullptr) { + return false; + } + + bufferView.buffer->SetStride(elementStride); + bufferView.buffer->SetBufferType(RHI::BufferType::Storage); + bufferView.buffer->SetState(RHI::ResourceStates::Common); + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = RHI::ResourceViewDimension::StructuredBuffer; + viewDesc.firstElement = 0u; + viewDesc.elementCount = elementCount; + viewDesc.structureByteStride = elementStride; + + bufferView.shaderResourceView = device->CreateShaderResourceView(bufferView.buffer, viewDesc); + if (bufferView.shaderResourceView == nullptr) { + return false; + } + + bufferView.unorderedAccessView = device->CreateUnorderedAccessView(bufferView.buffer, viewDesc); + if (bufferView.unorderedAccessView == nullptr) { + return false; + } + + bufferView.elementCount = elementCount; + bufferView.elementStride = elementStride; + bufferView.currentState = RHI::ResourceStates::Common; + return true; +} + +} // namespace + +BuiltinGaussianSplatPassResources::~BuiltinGaussianSplatPassResources() { + Shutdown(); +} + +bool BuiltinGaussianSplatPassResources::EnsureWorkingSet( + RHI::RHIDevice* device, + const VisibleGaussianSplatItem& visibleGaussianSplat, + WorkingSet*& outWorkingSet) { + outWorkingSet = nullptr; + if (device == nullptr || + visibleGaussianSplat.gaussianSplatRenderer == nullptr || + visibleGaussianSplat.gaussianSplat == nullptr || + !visibleGaussianSplat.gaussianSplat->IsValid()) { + return false; + } + + const Core::uint32 splatCapacity = visibleGaussianSplat.gaussianSplat->GetSplatCount(); + if (splatCapacity == 0u) { + return false; + } + + if (!ResetForDevice(device)) { + return false; + } + + WorkingSet& workingSet = m_workingSets[visibleGaussianSplat.gaussianSplatRenderer]; + if (workingSet.splatCapacity < splatCapacity) { + DestroyBufferView(workingSet.sortDistances); + DestroyBufferView(workingSet.orderIndices); + DestroyBufferView(workingSet.viewData); + if (!RecreateWorkingSet(device, splatCapacity, workingSet)) { + m_workingSets.erase(visibleGaussianSplat.gaussianSplatRenderer); + return false; + } + } + + workingSet.renderer = visibleGaussianSplat.gaussianSplatRenderer; + outWorkingSet = &workingSet; + return true; +} + +bool BuiltinGaussianSplatPassResources::EnsureAccumulationSurface( + RHI::RHIDevice* device, + Core::uint32 width, + Core::uint32 height, + RHI::Format format, + AccumulationSurface*& outSurface) { + outSurface = nullptr; + if (device == nullptr || + width == 0u || + height == 0u || + format == RHI::Format::Unknown) { + return false; + } + + if (!ResetForDevice(device)) { + return false; + } + + if (m_accumulationSurface.texture == nullptr || + m_accumulationSurface.width != width || + m_accumulationSurface.height != height || + m_accumulationSurface.format != format) { + if (!RecreateAccumulationSurface(device, width, height, format)) { + return false; + } + } + + outSurface = &m_accumulationSurface; + return true; +} + +void BuiltinGaussianSplatPassResources::Shutdown() { + for (auto& workingSetPair : m_workingSets) { + DestroyBufferView(workingSetPair.second.sortDistances); + DestroyBufferView(workingSetPair.second.orderIndices); + DestroyBufferView(workingSetPair.second.viewData); + } + + m_workingSets.clear(); + DestroyAccumulationSurface(m_accumulationSurface); + m_device = nullptr; +} + +const BuiltinGaussianSplatPassResources::WorkingSet* BuiltinGaussianSplatPassResources::FindWorkingSet( + const Components::GaussianSplatRendererComponent* renderer) const { + const auto workingSetIt = m_workingSets.find(renderer); + return workingSetIt != m_workingSets.end() ? &workingSetIt->second : nullptr; +} + +const BuiltinGaussianSplatPassResources::AccumulationSurface* BuiltinGaussianSplatPassResources::GetAccumulationSurface() const { + return m_accumulationSurface.texture != nullptr ? &m_accumulationSurface : nullptr; +} + +void BuiltinGaussianSplatPassResources::DestroyBufferView(CachedBufferView& bufferView) { + if (bufferView.shaderResourceView != nullptr) { + bufferView.shaderResourceView->Shutdown(); + delete bufferView.shaderResourceView; + bufferView.shaderResourceView = nullptr; + } + + if (bufferView.unorderedAccessView != nullptr) { + bufferView.unorderedAccessView->Shutdown(); + delete bufferView.unorderedAccessView; + bufferView.unorderedAccessView = nullptr; + } + + if (bufferView.buffer != nullptr) { + bufferView.buffer->Shutdown(); + delete bufferView.buffer; + bufferView.buffer = nullptr; + } + + bufferView.elementCount = 0u; + bufferView.elementStride = 0u; + bufferView.currentState = RHI::ResourceStates::Common; +} + +void BuiltinGaussianSplatPassResources::DestroyAccumulationSurface(AccumulationSurface& surface) { + if (surface.renderTargetView != nullptr) { + surface.renderTargetView->Shutdown(); + delete surface.renderTargetView; + surface.renderTargetView = nullptr; + } + + if (surface.shaderResourceView != nullptr) { + surface.shaderResourceView->Shutdown(); + delete surface.shaderResourceView; + surface.shaderResourceView = nullptr; + } + + if (surface.texture != nullptr) { + surface.texture->Shutdown(); + delete surface.texture; + surface.texture = nullptr; + } + + surface.width = 0u; + surface.height = 0u; + surface.format = RHI::Format::Unknown; + surface.currentColorState = RHI::ResourceStates::Common; +} + +bool BuiltinGaussianSplatPassResources::ResetForDevice(RHI::RHIDevice* device) { + if (m_device == nullptr) { + m_device = device; + return true; + } + + if (m_device == device) { + return true; + } + + Shutdown(); + m_device = device; + return true; +} + +bool BuiltinGaussianSplatPassResources::RecreateWorkingSet( + RHI::RHIDevice* device, + Core::uint32 splatCapacity, + WorkingSet& workingSet) { + if (!CreateStructuredBufferView(device, splatCapacity, kSortDistanceStride, workingSet.sortDistances) || + !CreateStructuredBufferView(device, splatCapacity, kOrderIndexStride, workingSet.orderIndices) || + !CreateStructuredBufferView(device, splatCapacity, kViewDataStride, workingSet.viewData)) { + DestroyBufferView(workingSet.sortDistances); + DestroyBufferView(workingSet.orderIndices); + DestroyBufferView(workingSet.viewData); + workingSet.renderer = nullptr; + workingSet.splatCapacity = 0u; + return false; + } + + workingSet.splatCapacity = splatCapacity; + return true; +} + +bool BuiltinGaussianSplatPassResources::CreateStructuredBufferView( + RHI::RHIDevice* device, + Core::uint32 elementCount, + Core::uint32 elementStride, + CachedBufferView& bufferView) { + return CreateStructuredBufferViews(device, elementCount, elementStride, bufferView); +} + +bool BuiltinGaussianSplatPassResources::RecreateAccumulationSurface( + RHI::RHIDevice* device, + Core::uint32 width, + Core::uint32 height, + RHI::Format format) { + DestroyAccumulationSurface(m_accumulationSurface); + + RHI::TextureDesc textureDesc = {}; + textureDesc.width = width; + textureDesc.height = height; + textureDesc.depth = 1u; + textureDesc.mipLevels = 1u; + textureDesc.arraySize = 1u; + textureDesc.format = static_cast(format); + textureDesc.textureType = static_cast(RHI::TextureType::Texture2D); + textureDesc.sampleCount = 1u; + textureDesc.sampleQuality = 0u; + textureDesc.flags = 0u; + + m_accumulationSurface.texture = device->CreateTexture(textureDesc); + if (m_accumulationSurface.texture == nullptr) { + return false; + } + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(format); + viewDesc.dimension = RHI::ResourceViewDimension::Texture2D; + viewDesc.mipLevel = 0u; + + m_accumulationSurface.renderTargetView = device->CreateRenderTargetView(m_accumulationSurface.texture, viewDesc); + if (m_accumulationSurface.renderTargetView == nullptr) { + DestroyAccumulationSurface(m_accumulationSurface); + return false; + } + + m_accumulationSurface.shaderResourceView = device->CreateShaderResourceView(m_accumulationSurface.texture, viewDesc); + if (m_accumulationSurface.shaderResourceView == nullptr) { + DestroyAccumulationSurface(m_accumulationSurface); + return false; + } + + m_accumulationSurface.width = width; + m_accumulationSurface.height = height; + m_accumulationSurface.format = format; + m_accumulationSurface.currentColorState = RHI::ResourceStates::Common; + return true; +} + +} // namespace Internal +} // namespace Passes +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h new file mode 100644 index 00000000..9a9c030f --- /dev/null +++ b/engine/src/Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace XCEngine { +namespace Rendering { +namespace Passes { +namespace Internal { + +struct GaussianSplatViewData { + Math::Vector4 clipCenter = Math::Vector4::Zero(); + Math::Vector4 ellipseAxisU = Math::Vector4::Zero(); + Math::Vector4 ellipseAxisV = Math::Vector4::Zero(); + Math::Vector4 colorOpacity = Math::Vector4::Zero(); +}; + +class BuiltinGaussianSplatPassResources final { +public: + struct CachedBufferView { + RHI::RHIBuffer* buffer = nullptr; + RHI::RHIResourceView* shaderResourceView = nullptr; + RHI::RHIResourceView* unorderedAccessView = nullptr; + Core::uint32 elementCount = 0u; + Core::uint32 elementStride = 0u; + RHI::ResourceStates currentState = RHI::ResourceStates::Common; + }; + + struct WorkingSet { + const Components::GaussianSplatRendererComponent* renderer = nullptr; + Core::uint32 splatCapacity = 0u; + CachedBufferView sortDistances = {}; + CachedBufferView orderIndices = {}; + CachedBufferView viewData = {}; + }; + + struct AccumulationSurface { + Core::uint32 width = 0u; + Core::uint32 height = 0u; + RHI::Format format = RHI::Format::Unknown; + RHI::RHITexture* texture = nullptr; + RHI::RHIResourceView* renderTargetView = nullptr; + RHI::RHIResourceView* shaderResourceView = nullptr; + RHI::ResourceStates currentColorState = RHI::ResourceStates::Common; + }; + + ~BuiltinGaussianSplatPassResources(); + + bool EnsureWorkingSet( + RHI::RHIDevice* device, + const VisibleGaussianSplatItem& visibleGaussianSplat, + WorkingSet*& outWorkingSet); + bool EnsureAccumulationSurface( + RHI::RHIDevice* device, + Core::uint32 width, + Core::uint32 height, + RHI::Format format, + AccumulationSurface*& outSurface); + void Shutdown(); + + size_t GetWorkingSetCount() const { return m_workingSets.size(); } + const WorkingSet* FindWorkingSet( + const Components::GaussianSplatRendererComponent* renderer) const; + const AccumulationSurface* GetAccumulationSurface() const; + +private: + static constexpr Core::uint32 kSortDistanceStride = sizeof(float); + static constexpr Core::uint32 kOrderIndexStride = sizeof(Core::uint32); + static constexpr Core::uint32 kViewDataStride = sizeof(GaussianSplatViewData); + + static void DestroyBufferView(CachedBufferView& bufferView); + static void DestroyAccumulationSurface(AccumulationSurface& surface); + + bool ResetForDevice(RHI::RHIDevice* device); + bool RecreateWorkingSet( + RHI::RHIDevice* device, + Core::uint32 splatCapacity, + WorkingSet& workingSet); + bool CreateStructuredBufferView( + RHI::RHIDevice* device, + Core::uint32 elementCount, + Core::uint32 elementStride, + CachedBufferView& bufferView); + bool RecreateAccumulationSurface( + RHI::RHIDevice* device, + Core::uint32 width, + Core::uint32 height, + RHI::Format format); + + RHI::RHIDevice* m_device = nullptr; + std::unordered_map m_workingSets; + AccumulationSurface m_accumulationSurface = {}; +}; + +} // namespace Internal +} // namespace Passes +} // namespace Rendering +} // namespace XCEngine diff --git a/tests/Rendering/unit/CMakeLists.txt b/tests/Rendering/unit/CMakeLists.txt index f0e259b3..f7c1b79e 100644 --- a/tests/Rendering/unit/CMakeLists.txt +++ b/tests/Rendering/unit/CMakeLists.txt @@ -6,6 +6,7 @@ set(RENDERING_UNIT_TEST_SOURCES test_render_pass.cpp test_object_id_encoding.cpp test_builtin_forward_pipeline.cpp + test_builtin_gaussian_splat_pass_resources.cpp test_camera_scene_renderer.cpp test_scene_render_request_planner.cpp test_scene_render_request_utils.cpp diff --git a/tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp b/tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp new file mode 100644 index 00000000..9926d96c --- /dev/null +++ b/tests/Rendering/unit/test_builtin_gaussian_splat_pass_resources.cpp @@ -0,0 +1,441 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h" + +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Rendering; +using namespace XCEngine::Rendering::Passes::Internal; +using namespace XCEngine::Resources; + +namespace { + +struct MockGaussianSplatResourceState { + int createBufferCalls = 0; + int bufferShutdownCalls = 0; + int bufferDestroyCalls = 0; + int createBufferShaderViewCalls = 0; + int createBufferUavCalls = 0; + int createTextureCalls = 0; + int textureShutdownCalls = 0; + int textureDestroyCalls = 0; + int createTextureShaderViewCalls = 0; + int createRenderTargetViewCalls = 0; + int resourceViewShutdownCalls = 0; + int resourceViewDestroyCalls = 0; +}; + +class MockGaussianSplatBuffer final : public XCEngine::RHI::RHIBuffer { +public: + MockGaussianSplatBuffer( + std::shared_ptr state, + const XCEngine::RHI::BufferDesc& desc) + : m_state(std::move(state)) + , m_size(desc.size) + , m_stride(desc.stride) + , m_type(static_cast(desc.bufferType)) { + } + + ~MockGaussianSplatBuffer() override { + ++m_state->bufferDestroyCalls; + } + + void* Map() override { return nullptr; } + void Unmap() override {} + void SetData(const void*, size_t, size_t) override {} + uint64_t GetSize() const override { return m_size; } + XCEngine::RHI::BufferType GetBufferType() const override { return m_type; } + void SetBufferType(XCEngine::RHI::BufferType type) override { m_type = type; } + uint32_t GetStride() const override { return m_stride; } + void SetStride(uint32_t stride) override { m_stride = stride; } + void* GetNativeHandle() override { return nullptr; } + XCEngine::RHI::ResourceStates GetState() const override { return m_resourceState; } + void SetState(XCEngine::RHI::ResourceStates state) override { m_resourceState = state; } + const std::string& GetName() const override { return m_name; } + void SetName(const std::string& name) override { m_name = name; } + + void Shutdown() override { + ++m_state->bufferShutdownCalls; + } + +private: + std::shared_ptr m_state; + uint64_t m_size = 0u; + uint32_t m_stride = 0u; + XCEngine::RHI::BufferType m_type = XCEngine::RHI::BufferType::Storage; + XCEngine::RHI::ResourceStates m_resourceState = XCEngine::RHI::ResourceStates::Common; + std::string m_name; +}; + +class MockGaussianSplatTexture final : public XCEngine::RHI::RHITexture { +public: + MockGaussianSplatTexture( + std::shared_ptr state, + const XCEngine::RHI::TextureDesc& desc) + : m_state(std::move(state)) + , m_width(desc.width) + , m_height(desc.height) + , m_format(static_cast(desc.format)) + , m_type(static_cast(desc.textureType)) { + } + + ~MockGaussianSplatTexture() override { + ++m_state->textureDestroyCalls; + } + + uint32_t GetWidth() const override { return m_width; } + uint32_t GetHeight() const override { return m_height; } + uint32_t GetDepth() const override { return 1u; } + uint32_t GetMipLevels() const override { return 1u; } + XCEngine::RHI::Format GetFormat() const override { return m_format; } + XCEngine::RHI::TextureType GetTextureType() const override { return m_type; } + XCEngine::RHI::ResourceStates GetState() const override { return m_resourceState; } + void SetState(XCEngine::RHI::ResourceStates state) override { m_resourceState = state; } + void* GetNativeHandle() override { return nullptr; } + const std::string& GetName() const override { return m_name; } + void SetName(const std::string& name) override { m_name = name; } + + void Shutdown() override { + ++m_state->textureShutdownCalls; + } + +private: + std::shared_ptr m_state; + uint32_t m_width = 0u; + uint32_t m_height = 0u; + XCEngine::RHI::Format m_format = XCEngine::RHI::Format::Unknown; + XCEngine::RHI::TextureType m_type = XCEngine::RHI::TextureType::Texture2D; + XCEngine::RHI::ResourceStates m_resourceState = XCEngine::RHI::ResourceStates::Common; + std::string m_name; +}; + +class MockGaussianSplatResourceView final : public XCEngine::RHI::RHIResourceView { +public: + MockGaussianSplatResourceView( + std::shared_ptr state, + XCEngine::RHI::ResourceViewType type, + XCEngine::RHI::Format format, + XCEngine::RHI::ResourceViewDimension dimension) + : m_state(std::move(state)) + , m_type(type) + , m_format(format) + , m_dimension(dimension) { + } + + ~MockGaussianSplatResourceView() override { + ++m_state->resourceViewDestroyCalls; + } + + void Shutdown() override { + ++m_state->resourceViewShutdownCalls; + } + + void* GetNativeHandle() override { return nullptr; } + bool IsValid() const override { return true; } + XCEngine::RHI::ResourceViewType GetViewType() const override { return m_type; } + XCEngine::RHI::ResourceViewDimension GetDimension() const override { return m_dimension; } + XCEngine::RHI::Format GetFormat() const override { return m_format; } + +private: + std::shared_ptr m_state; + XCEngine::RHI::ResourceViewType m_type = XCEngine::RHI::ResourceViewType::ShaderResource; + XCEngine::RHI::Format m_format = XCEngine::RHI::Format::Unknown; + XCEngine::RHI::ResourceViewDimension m_dimension = XCEngine::RHI::ResourceViewDimension::Unknown; +}; + +class MockGaussianSplatDevice final : public XCEngine::RHI::RHIDevice { +public: + explicit MockGaussianSplatDevice(std::shared_ptr state) + : m_state(std::move(state)) { + } + + bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; } + void Shutdown() override {} + + XCEngine::RHI::RHIBuffer* CreateBuffer(const XCEngine::RHI::BufferDesc& desc) override { + ++m_state->createBufferCalls; + return new MockGaussianSplatBuffer(m_state, desc); + } + + XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc& desc) override { + ++m_state->createTextureCalls; + return new MockGaussianSplatTexture(m_state, desc); + } + + XCEngine::RHI::RHITexture* CreateTexture( + const XCEngine::RHI::TextureDesc& desc, + const void*, + size_t, + uint32_t) override { + return CreateTexture(desc); + } + + XCEngine::RHI::RHISwapChain* CreateSwapChain( + const XCEngine::RHI::SwapChainDesc&, + XCEngine::RHI::RHICommandQueue*) override { return nullptr; } + XCEngine::RHI::RHICommandList* CreateCommandList(const XCEngine::RHI::CommandListDesc&) override { return nullptr; } + XCEngine::RHI::RHICommandQueue* CreateCommandQueue(const XCEngine::RHI::CommandQueueDesc&) override { return nullptr; } + XCEngine::RHI::RHIShader* CreateShader(const XCEngine::RHI::ShaderCompileDesc&) override { return nullptr; } + XCEngine::RHI::RHIPipelineState* CreatePipelineState(const XCEngine::RHI::GraphicsPipelineDesc&) override { return nullptr; } + XCEngine::RHI::RHIPipelineLayout* CreatePipelineLayout(const XCEngine::RHI::RHIPipelineLayoutDesc&) override { return nullptr; } + XCEngine::RHI::RHIFence* CreateFence(const XCEngine::RHI::FenceDesc&) override { return nullptr; } + XCEngine::RHI::RHISampler* CreateSampler(const XCEngine::RHI::SamplerDesc&) override { return nullptr; } + XCEngine::RHI::RHIRenderPass* CreateRenderPass( + uint32_t, + const XCEngine::RHI::AttachmentDesc*, + const XCEngine::RHI::AttachmentDesc*) override { return nullptr; } + XCEngine::RHI::RHIFramebuffer* CreateFramebuffer( + XCEngine::RHI::RHIRenderPass*, + uint32_t, + uint32_t, + uint32_t, + XCEngine::RHI::RHIResourceView**, + XCEngine::RHI::RHIResourceView*) override { return nullptr; } + XCEngine::RHI::RHIDescriptorPool* CreateDescriptorPool(const XCEngine::RHI::DescriptorPoolDesc&) override { return nullptr; } + XCEngine::RHI::RHIDescriptorSet* CreateDescriptorSet( + XCEngine::RHI::RHIDescriptorPool*, + const XCEngine::RHI::DescriptorSetLayoutDesc&) override { return nullptr; } + XCEngine::RHI::RHIResourceView* CreateVertexBufferView( + XCEngine::RHI::RHIBuffer*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } + XCEngine::RHI::RHIResourceView* CreateIndexBufferView( + XCEngine::RHI::RHIBuffer*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } + + XCEngine::RHI::RHIResourceView* CreateRenderTargetView( + XCEngine::RHI::RHITexture*, + const XCEngine::RHI::ResourceViewDesc& desc) override { + ++m_state->createRenderTargetViewCalls; + return new MockGaussianSplatResourceView( + m_state, + XCEngine::RHI::ResourceViewType::RenderTarget, + static_cast(desc.format), + desc.dimension); + } + + XCEngine::RHI::RHIResourceView* CreateDepthStencilView( + XCEngine::RHI::RHITexture*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } + + XCEngine::RHI::RHIResourceView* CreateShaderResourceView( + XCEngine::RHI::RHIBuffer*, + const XCEngine::RHI::ResourceViewDesc& desc) override { + ++m_state->createBufferShaderViewCalls; + return new MockGaussianSplatResourceView( + m_state, + XCEngine::RHI::ResourceViewType::ShaderResource, + static_cast(desc.format), + desc.dimension); + } + + XCEngine::RHI::RHIResourceView* CreateShaderResourceView( + XCEngine::RHI::RHITexture*, + const XCEngine::RHI::ResourceViewDesc& desc) override { + ++m_state->createTextureShaderViewCalls; + return new MockGaussianSplatResourceView( + m_state, + XCEngine::RHI::ResourceViewType::ShaderResource, + static_cast(desc.format), + desc.dimension); + } + + XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView( + XCEngine::RHI::RHIBuffer*, + const XCEngine::RHI::ResourceViewDesc& desc) override { + ++m_state->createBufferUavCalls; + return new MockGaussianSplatResourceView( + m_state, + XCEngine::RHI::ResourceViewType::UnorderedAccess, + static_cast(desc.format), + desc.dimension); + } + + XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView( + XCEngine::RHI::RHITexture*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } + + const XCEngine::RHI::RHICapabilities& GetCapabilities() const override { return m_capabilities; } + const XCEngine::RHI::RHIDeviceInfo& GetDeviceInfo() const override { return m_info; } + void* GetNativeDevice() override { return nullptr; } + +private: + std::shared_ptr m_state; + XCEngine::RHI::RHICapabilities m_capabilities = {}; + XCEngine::RHI::RHIDeviceInfo m_info = {}; +}; + +GaussianSplat* CreateTestGaussianSplat(const char* path, XCEngine::Core::uint32 splatCount) { + auto* gaussianSplat = new GaussianSplat(); + IResource::ConstructParams params = {}; + params.name = "TestGaussianSplat"; + params.path = path; + params.guid = ResourceGUID::Generate(path); + gaussianSplat->Initialize(params); + + GaussianSplatMetadata metadata = {}; + metadata.splatCount = splatCount; + + XCEngine::Containers::Array sections; + sections.Resize(1); + sections[0].type = GaussianSplatSectionType::Positions; + sections[0].format = GaussianSplatSectionFormat::VectorFloat32; + sections[0].dataOffset = 0u; + sections[0].dataSize = static_cast(splatCount) * sizeof(GaussianSplatPositionRecord); + sections[0].elementCount = splatCount; + sections[0].elementStride = sizeof(GaussianSplatPositionRecord); + + XCEngine::Containers::Array payload; + payload.Resize(static_cast(sections[0].dataSize)); + for (XCEngine::Core::uint32 index = 0; index < splatCount; ++index) { + const GaussianSplatPositionRecord positionRecord = { XCEngine::Math::Vector3(static_cast(index), 0.0f, 0.0f) }; + std::memcpy( + payload.Data() + (static_cast(index) * sizeof(GaussianSplatPositionRecord)), + &positionRecord, + sizeof(positionRecord)); + } + + EXPECT_TRUE(gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))); + return gaussianSplat; +} + +VisibleGaussianSplatItem BuildVisibleGaussianSplatItem( + GameObject& gameObject, + GaussianSplatRendererComponent& renderer, + GaussianSplat& gaussianSplat) { + VisibleGaussianSplatItem item = {}; + item.gameObject = &gameObject; + item.gaussianSplatRenderer = &renderer; + item.gaussianSplat = &gaussianSplat; + return item; +} + +TEST(BuiltinGaussianSplatPassResources_Test, EnsureWorkingSetAllocatesAndReusesStructuredBuffersPerRenderer) { + auto state = std::make_shared(); + MockGaussianSplatDevice device(state); + BuiltinGaussianSplatPassResources resources; + + GameObject gameObject("GaussianSplatObject"); + auto* renderer = gameObject.AddComponent(); + std::unique_ptr gaussianSplat(CreateTestGaussianSplat("GaussianSplats/room.xcgsplat", 8u)); + VisibleGaussianSplatItem item = BuildVisibleGaussianSplatItem(gameObject, *renderer, *gaussianSplat); + + BuiltinGaussianSplatPassResources::WorkingSet* workingSet = nullptr; + ASSERT_TRUE(resources.EnsureWorkingSet(&device, item, workingSet)); + ASSERT_NE(workingSet, nullptr); + EXPECT_EQ(resources.GetWorkingSetCount(), 1u); + EXPECT_EQ(workingSet->renderer, renderer); + EXPECT_EQ(workingSet->splatCapacity, 8u); + EXPECT_EQ(workingSet->sortDistances.elementStride, sizeof(float)); + EXPECT_EQ(workingSet->orderIndices.elementStride, sizeof(XCEngine::Core::uint32)); + EXPECT_EQ(workingSet->viewData.elementStride, sizeof(GaussianSplatViewData)); + EXPECT_EQ(workingSet->sortDistances.shaderResourceView->GetDimension(), XCEngine::RHI::ResourceViewDimension::StructuredBuffer); + EXPECT_EQ(workingSet->sortDistances.unorderedAccessView->GetViewType(), XCEngine::RHI::ResourceViewType::UnorderedAccess); + EXPECT_EQ(state->createBufferCalls, 3); + EXPECT_EQ(state->createBufferShaderViewCalls, 3); + EXPECT_EQ(state->createBufferUavCalls, 3); + + BuiltinGaussianSplatPassResources::WorkingSet* reusedWorkingSet = nullptr; + ASSERT_TRUE(resources.EnsureWorkingSet(&device, item, reusedWorkingSet)); + EXPECT_EQ(reusedWorkingSet, workingSet); + EXPECT_EQ(state->createBufferCalls, 3); + EXPECT_EQ(state->createBufferShaderViewCalls, 3); + EXPECT_EQ(state->createBufferUavCalls, 3); +} + +TEST(BuiltinGaussianSplatPassResources_Test, EnsureWorkingSetKeepsPerRendererIsolationAndRecreatesOnCapacityGrowth) { + auto state = std::make_shared(); + MockGaussianSplatDevice device(state); + BuiltinGaussianSplatPassResources resources; + + GameObject firstObject("FirstGaussianSplat"); + auto* firstRenderer = firstObject.AddComponent(); + std::unique_ptr firstGaussianSplat(CreateTestGaussianSplat("GaussianSplats/first.xcgsplat", 4u)); + VisibleGaussianSplatItem firstItem = BuildVisibleGaussianSplatItem(firstObject, *firstRenderer, *firstGaussianSplat); + + GameObject secondObject("SecondGaussianSplat"); + auto* secondRenderer = secondObject.AddComponent(); + std::unique_ptr secondGaussianSplat(CreateTestGaussianSplat("GaussianSplats/second.xcgsplat", 4u)); + VisibleGaussianSplatItem secondItem = BuildVisibleGaussianSplatItem(secondObject, *secondRenderer, *secondGaussianSplat); + + BuiltinGaussianSplatPassResources::WorkingSet* firstWorkingSet = nullptr; + BuiltinGaussianSplatPassResources::WorkingSet* secondWorkingSet = nullptr; + ASSERT_TRUE(resources.EnsureWorkingSet(&device, firstItem, firstWorkingSet)); + ASSERT_TRUE(resources.EnsureWorkingSet(&device, secondItem, secondWorkingSet)); + ASSERT_NE(firstWorkingSet, nullptr); + ASSERT_NE(secondWorkingSet, nullptr); + EXPECT_NE(firstWorkingSet, secondWorkingSet); + EXPECT_EQ(resources.GetWorkingSetCount(), 2u); + EXPECT_EQ(state->createBufferCalls, 6); + + std::unique_ptr grownGaussianSplat(CreateTestGaussianSplat("GaussianSplats/first_grown.xcgsplat", 12u)); + firstItem.gaussianSplat = grownGaussianSplat.get(); + + BuiltinGaussianSplatPassResources::WorkingSet* grownWorkingSet = nullptr; + ASSERT_TRUE(resources.EnsureWorkingSet(&device, firstItem, grownWorkingSet)); + ASSERT_NE(grownWorkingSet, nullptr); + EXPECT_EQ(grownWorkingSet, resources.FindWorkingSet(firstRenderer)); + EXPECT_EQ(grownWorkingSet->splatCapacity, 12u); + EXPECT_EQ(resources.GetWorkingSetCount(), 2u); + EXPECT_EQ(state->createBufferCalls, 9); + EXPECT_GE(state->bufferShutdownCalls, 3); + EXPECT_GE(state->bufferDestroyCalls, 3); +} + +TEST(BuiltinGaussianSplatPassResources_Test, EnsureAccumulationSurfaceReusesCompatibleTargetAndRecreatesOnResize) { + auto state = std::make_shared(); + MockGaussianSplatDevice device(state); + BuiltinGaussianSplatPassResources resources; + + BuiltinGaussianSplatPassResources::AccumulationSurface* surface = nullptr; + ASSERT_TRUE(resources.EnsureAccumulationSurface( + &device, + 640u, + 360u, + XCEngine::RHI::Format::R16G16B16A16_Float, + surface)); + ASSERT_NE(surface, nullptr); + EXPECT_EQ(surface->width, 640u); + EXPECT_EQ(surface->height, 360u); + EXPECT_EQ(surface->format, XCEngine::RHI::Format::R16G16B16A16_Float); + EXPECT_EQ(state->createTextureCalls, 1); + EXPECT_EQ(state->createRenderTargetViewCalls, 1); + EXPECT_EQ(state->createTextureShaderViewCalls, 1); + + BuiltinGaussianSplatPassResources::AccumulationSurface* reusedSurface = nullptr; + ASSERT_TRUE(resources.EnsureAccumulationSurface( + &device, + 640u, + 360u, + XCEngine::RHI::Format::R16G16B16A16_Float, + reusedSurface)); + EXPECT_EQ(reusedSurface, surface); + EXPECT_EQ(state->createTextureCalls, 1); + + BuiltinGaussianSplatPassResources::AccumulationSurface* resizedSurface = nullptr; + ASSERT_TRUE(resources.EnsureAccumulationSurface( + &device, + 800u, + 600u, + XCEngine::RHI::Format::R16G16B16A16_Float, + resizedSurface)); + ASSERT_NE(resizedSurface, nullptr); + EXPECT_EQ(resizedSurface->width, 800u); + EXPECT_EQ(resizedSurface->height, 600u); + EXPECT_EQ(state->createTextureCalls, 2); + EXPECT_GE(state->textureShutdownCalls, 1); + EXPECT_GE(state->textureDestroyCalls, 1); + EXPECT_GE(state->resourceViewShutdownCalls, 2); + EXPECT_GE(state->resourceViewDestroyCalls, 2); +} + +} // namespace