Formalize GaussianSplat transient pass resources

This commit is contained in:
2026-04-10 22:15:05 +08:00
parent 977a4cf2a4
commit 15b42c248f
5 changed files with 844 additions and 0 deletions

View File

@@ -526,6 +526,7 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinObjectIdOutlinePass.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/BuiltinSelectionOutlinePass.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinVolumetricPass.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/RenderSurface.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Extraction/RenderSceneExtractor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Extraction/RenderSceneExtractor.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Extraction/RenderSceneUtility.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Extraction/RenderSceneUtility.cpp

View File

@@ -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<Core::uint64>(elementCount) * static_cast<Core::uint64>(elementStride);
bufferDesc.stride = elementStride;
bufferDesc.bufferType = static_cast<uint32_t>(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<uint32_t>(format);
textureDesc.textureType = static_cast<uint32_t>(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<uint32_t>(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

View File

@@ -0,0 +1,104 @@
#pragma once
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Core/Types.h>
#include <XCEngine/Rendering/FrameData/VisibleGaussianSplatItem.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHITexture.h>
#include <unordered_map>
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<const Components::GaussianSplatRendererComponent*, WorkingSet> m_workingSets;
AccumulationSurface m_accumulationSurface = {};
};
} // namespace Internal
} // namespace Passes
} // namespace Rendering
} // namespace XCEngine

View File

@@ -6,6 +6,7 @@ set(RENDERING_UNIT_TEST_SOURCES
test_render_pass.cpp test_render_pass.cpp
test_object_id_encoding.cpp test_object_id_encoding.cpp
test_builtin_forward_pipeline.cpp test_builtin_forward_pipeline.cpp
test_builtin_gaussian_splat_pass_resources.cpp
test_camera_scene_renderer.cpp test_camera_scene_renderer.cpp
test_scene_render_request_planner.cpp test_scene_render_request_planner.cpp
test_scene_render_request_utils.cpp test_scene_render_request_utils.cpp

View File

@@ -0,0 +1,441 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/GaussianSplatRendererComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHITexture.h>
#include "Rendering/Passes/Internal/BuiltinGaussianSplatPassResources.h"
#include <cstring>
#include <memory>
#include <string>
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<MockGaussianSplatResourceState> state,
const XCEngine::RHI::BufferDesc& desc)
: m_state(std::move(state))
, m_size(desc.size)
, m_stride(desc.stride)
, m_type(static_cast<XCEngine::RHI::BufferType>(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<MockGaussianSplatResourceState> 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<MockGaussianSplatResourceState> state,
const XCEngine::RHI::TextureDesc& desc)
: m_state(std::move(state))
, m_width(desc.width)
, m_height(desc.height)
, m_format(static_cast<XCEngine::RHI::Format>(desc.format))
, m_type(static_cast<XCEngine::RHI::TextureType>(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<MockGaussianSplatResourceState> 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<MockGaussianSplatResourceState> 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<MockGaussianSplatResourceState> 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<MockGaussianSplatResourceState> 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<XCEngine::RHI::Format>(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<XCEngine::RHI::Format>(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<XCEngine::RHI::Format>(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<XCEngine::RHI::Format>(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<MockGaussianSplatResourceState> 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<GaussianSplatSection> sections;
sections.Resize(1);
sections[0].type = GaussianSplatSectionType::Positions;
sections[0].format = GaussianSplatSectionFormat::VectorFloat32;
sections[0].dataOffset = 0u;
sections[0].dataSize = static_cast<XCEngine::Core::uint64>(splatCount) * sizeof(GaussianSplatPositionRecord);
sections[0].elementCount = splatCount;
sections[0].elementStride = sizeof(GaussianSplatPositionRecord);
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
payload.Resize(static_cast<size_t>(sections[0].dataSize));
for (XCEngine::Core::uint32 index = 0; index < splatCount; ++index) {
const GaussianSplatPositionRecord positionRecord = { XCEngine::Math::Vector3(static_cast<float>(index), 0.0f, 0.0f) };
std::memcpy(
payload.Data() + (static_cast<size_t>(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<MockGaussianSplatResourceState>();
MockGaussianSplatDevice device(state);
BuiltinGaussianSplatPassResources resources;
GameObject gameObject("GaussianSplatObject");
auto* renderer = gameObject.AddComponent<GaussianSplatRendererComponent>();
std::unique_ptr<GaussianSplat> 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<MockGaussianSplatResourceState>();
MockGaussianSplatDevice device(state);
BuiltinGaussianSplatPassResources resources;
GameObject firstObject("FirstGaussianSplat");
auto* firstRenderer = firstObject.AddComponent<GaussianSplatRendererComponent>();
std::unique_ptr<GaussianSplat> firstGaussianSplat(CreateTestGaussianSplat("GaussianSplats/first.xcgsplat", 4u));
VisibleGaussianSplatItem firstItem = BuildVisibleGaussianSplatItem(firstObject, *firstRenderer, *firstGaussianSplat);
GameObject secondObject("SecondGaussianSplat");
auto* secondRenderer = secondObject.AddComponent<GaussianSplatRendererComponent>();
std::unique_ptr<GaussianSplat> 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<GaussianSplat> 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<MockGaussianSplatResourceState>();
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