diff --git a/engine/include/XCEngine/Rendering/Caches/RenderResourceCache.h b/engine/include/XCEngine/Rendering/Caches/RenderResourceCache.h index ae32168e..46541a94 100644 --- a/engine/include/XCEngine/Rendering/Caches/RenderResourceCache.h +++ b/engine/include/XCEngine/Rendering/Caches/RenderResourceCache.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,14 @@ namespace Rendering { class RenderResourceCache { public: + enum class GaussianSplatResidencyState { + Uninitialized = 0, + CpuReady, + GpuUploading, + GpuReady, + Failed + }; + struct CachedMesh { RHI::RHIBuffer* vertexBuffer = nullptr; RHI::RHIResourceView* vertexBufferView = nullptr; @@ -46,6 +55,30 @@ public: Resources::VolumeStorageKind storageKind = Resources::VolumeStorageKind::Unknown; }; + struct CachedGaussianSplatSection { + RHI::RHIBuffer* buffer = nullptr; + RHI::RHIResourceView* shaderResourceView = nullptr; + Resources::GaussianSplatSectionType type = Resources::GaussianSplatSectionType::Unknown; + Resources::GaussianSplatSectionFormat format = Resources::GaussianSplatSectionFormat::Unknown; + uint32_t elementStride = 0; + uint32_t elementCount = 0; + uint64_t payloadSize = 0; + }; + + struct CachedGaussianSplat { + GaussianSplatResidencyState residencyState = GaussianSplatResidencyState::Uninitialized; + uint32_t contentVersion = 0; + uint32_t splatCount = 0; + uint32_t chunkCount = 0; + uint32_t cameraCount = 0; + Math::Bounds bounds; + CachedGaussianSplatSection positions; + CachedGaussianSplatSection other; + CachedGaussianSplatSection color; + CachedGaussianSplatSection sh; + CachedGaussianSplatSection chunks; + }; + ~RenderResourceCache(); void Shutdown(); @@ -55,6 +88,9 @@ public: const CachedVolumeField* GetOrCreateVolumeField( RHI::RHIDevice* device, const Resources::VolumeField* volumeField); + const CachedGaussianSplat* GetOrCreateGaussianSplat( + RHI::RHIDevice* device, + const Resources::GaussianSplat* gaussianSplat); const CachedBufferView* GetOrCreateBufferView( RHI::RHIDevice* device, RHI::RHIBuffer* buffer, @@ -94,6 +130,18 @@ private: RHI::RHIDevice* device, const Resources::VolumeField* volumeField, CachedVolumeField& cachedVolumeField); + bool UploadGaussianSplat( + RHI::RHIDevice* device, + const Resources::GaussianSplat* gaussianSplat, + CachedGaussianSplat& cachedGaussianSplat); + bool UploadGaussianSplatSection( + RHI::RHIDevice* device, + const Resources::GaussianSplat* gaussianSplat, + Resources::GaussianSplatSectionType sectionType, + Resources::GaussianSplatSectionFormat requiredFormat, + uint32_t requiredStride, + bool requiredSection, + CachedGaussianSplatSection& cachedSection); bool CreateBufferView( RHI::RHIDevice* device, RHI::RHIBuffer* buffer, @@ -104,6 +152,7 @@ private: std::unordered_map m_meshCache; std::unordered_map m_textureCache; std::unordered_map m_volumeFieldCache; + std::unordered_map m_gaussianSplatCache; std::unordered_map m_bufferViewCache; }; diff --git a/engine/src/Rendering/Caches/RenderResourceCache.cpp b/engine/src/Rendering/Caches/RenderResourceCache.cpp index 11d98741..61329198 100644 --- a/engine/src/Rendering/Caches/RenderResourceCache.cpp +++ b/engine/src/Rendering/Caches/RenderResourceCache.cpp @@ -1,11 +1,15 @@ #include "Rendering/Caches/RenderResourceCache.h" +#include "Debug/Logger.h" + #include #include +#include #include #include #include +#include #include namespace XCEngine { @@ -13,6 +17,21 @@ namespace Rendering { namespace { +constexpr size_t kVolumeWordStride = sizeof(uint32_t); + +uint64_t GetVolumeTraceSteadyMs() { + using Clock = std::chrono::steady_clock; + static const Clock::time_point s_start = Clock::now(); + return static_cast(std::chrono::duration_cast( + Clock::now() - s_start).count()); +} + +void LogVolumeTraceRendering(const std::string& message) { + Containers::String entry("[VolumeTrace] "); + entry += message.c_str(); + Debug::Logger::Get().Info(Debug::LogCategory::Rendering, entry); +} + template TValue AlignUp(TValue value, TValue alignment) { if (alignment == 0) { @@ -165,6 +184,58 @@ void ShutdownVolumeField(RenderResourceCache::CachedVolumeField& cachedVolumeFie } } +void ShutdownGaussianSplatSection(RenderResourceCache::CachedGaussianSplatSection& cachedSection) { + if (cachedSection.shaderResourceView != nullptr) { + cachedSection.shaderResourceView->Shutdown(); + delete cachedSection.shaderResourceView; + cachedSection.shaderResourceView = nullptr; + } + + if (cachedSection.buffer != nullptr) { + cachedSection.buffer->Shutdown(); + delete cachedSection.buffer; + cachedSection.buffer = nullptr; + } + + cachedSection.type = Resources::GaussianSplatSectionType::Unknown; + cachedSection.format = Resources::GaussianSplatSectionFormat::Unknown; + cachedSection.elementStride = 0u; + cachedSection.elementCount = 0u; + cachedSection.payloadSize = 0u; +} + +void ShutdownGaussianSplat(RenderResourceCache::CachedGaussianSplat& cachedGaussianSplat) { + ShutdownGaussianSplatSection(cachedGaussianSplat.positions); + ShutdownGaussianSplatSection(cachedGaussianSplat.other); + ShutdownGaussianSplatSection(cachedGaussianSplat.color); + ShutdownGaussianSplatSection(cachedGaussianSplat.sh); + ShutdownGaussianSplatSection(cachedGaussianSplat.chunks); + cachedGaussianSplat.contentVersion = 0u; + cachedGaussianSplat.splatCount = 0u; + cachedGaussianSplat.chunkCount = 0u; + cachedGaussianSplat.cameraCount = 0u; + cachedGaussianSplat.bounds = Math::Bounds(); + cachedGaussianSplat.residencyState = RenderResourceCache::GaussianSplatResidencyState::Uninitialized; +} + +bool ValidateGaussianSplatSectionLayout( + const Resources::GaussianSplatSection& section, + Resources::GaussianSplatSectionFormat requiredFormat, + uint32_t requiredStride, + uint32_t expectedElementCount) { + if (section.format != requiredFormat || section.elementStride != requiredStride) { + return false; + } + + if (section.elementCount != expectedElementCount) { + return false; + } + + const uint64_t expectedDataSize = + static_cast(section.elementCount) * static_cast(section.elementStride); + return expectedDataSize == section.dataSize; +} + } // namespace RenderResourceCache::~RenderResourceCache() { @@ -187,6 +258,11 @@ void RenderResourceCache::Shutdown() { } m_volumeFieldCache.clear(); + for (auto& entry : m_gaussianSplatCache) { + ShutdownGaussianSplat(entry.second); + } + m_gaussianSplatCache.clear(); + for (auto& entry : m_bufferViewCache) { ShutdownBufferView(entry.second); } @@ -263,6 +339,37 @@ const RenderResourceCache::CachedVolumeField* RenderResourceCache::GetOrCreateVo return &result.first->second; } +const RenderResourceCache::CachedGaussianSplat* RenderResourceCache::GetOrCreateGaussianSplat( + RHI::RHIDevice* device, + const Resources::GaussianSplat* gaussianSplat) { + if (device == nullptr || + gaussianSplat == nullptr || + !gaussianSplat->IsValid() || + gaussianSplat->GetSplatCount() == 0u || + gaussianSplat->GetPayloadData() == nullptr || + gaussianSplat->GetPayloadSize() == 0u) { + return nullptr; + } + + auto existing = m_gaussianSplatCache.find(gaussianSplat); + if (existing != m_gaussianSplatCache.end()) { + return existing->second.residencyState == GaussianSplatResidencyState::GpuReady + ? &existing->second + : nullptr; + } + + const auto result = m_gaussianSplatCache.emplace(gaussianSplat, CachedGaussianSplat{}); + CachedGaussianSplat& cachedGaussianSplat = result.first->second; + cachedGaussianSplat.residencyState = GaussianSplatResidencyState::CpuReady; + if (!UploadGaussianSplat(device, gaussianSplat, cachedGaussianSplat)) { + ShutdownGaussianSplat(cachedGaussianSplat); + cachedGaussianSplat.residencyState = GaussianSplatResidencyState::Failed; + return nullptr; + } + + return &cachedGaussianSplat; +} + const RenderResourceCache::CachedBufferView* RenderResourceCache::GetOrCreateBufferView( RHI::RHIDevice* device, RHI::RHIBuffer* buffer, @@ -436,29 +543,37 @@ bool RenderResourceCache::UploadVolumeField( return false; } - constexpr uint32_t kVolumeWordStride = sizeof(uint32_t); const size_t alignedPayloadSize = AlignUp(volumeField->GetPayloadSize(), static_cast(kVolumeWordStride)); if (alignedPayloadSize == 0u || alignedPayloadSize > static_cast(UINT64_MAX)) { return false; } + const uint64_t uploadStartMs = GetVolumeTraceSteadyMs(); + LogVolumeTraceRendering( + "UploadVolumeField begin path=" + std::string(volumeField->GetPath().CStr()) + + " steady_ms=" + std::to_string(uploadStartMs) + + " payload_bytes=" + std::to_string(volumeField->GetPayloadSize()) + + " aligned_bytes=" + std::to_string(alignedPayloadSize)); + RHI::BufferDesc bufferDesc = {}; bufferDesc.size = static_cast(alignedPayloadSize); bufferDesc.stride = kVolumeWordStride; bufferDesc.bufferType = static_cast(RHI::BufferType::Storage); bufferDesc.flags = 0; - cachedVolumeField.payloadBuffer = device->CreateBuffer(bufferDesc); + const uint64_t createBufferStartMs = GetVolumeTraceSteadyMs(); + cachedVolumeField.payloadBuffer = device->CreateBuffer( + bufferDesc, + volumeField->GetPayloadData(), + volumeField->GetPayloadSize(), + RHI::ResourceStates::GenericRead); if (cachedVolumeField.payloadBuffer == nullptr) { + LogVolumeTraceRendering( + "UploadVolumeField failed path=" + std::string(volumeField->GetPath().CStr()) + + " stage=create_buffer"); return false; } - - std::vector uploadData(alignedPayloadSize, 0u); - std::memcpy( - uploadData.data(), - volumeField->GetPayloadData(), - volumeField->GetPayloadSize()); - cachedVolumeField.payloadBuffer->SetData(uploadData.data(), uploadData.size()); + const uint64_t createBufferEndMs = GetVolumeTraceSteadyMs(); cachedVolumeField.payloadBuffer->SetStride(kVolumeWordStride); cachedVolumeField.payloadBuffer->SetBufferType(RHI::BufferType::Storage); @@ -471,8 +586,17 @@ bool RenderResourceCache::UploadVolumeField( cachedVolumeField.shaderResourceView = device->CreateShaderResourceView(cachedVolumeField.payloadBuffer, viewDesc); if (cachedVolumeField.shaderResourceView == nullptr) { + LogVolumeTraceRendering( + "UploadVolumeField failed path=" + std::string(volumeField->GetPath().CStr()) + + " stage=create_srv"); return false; } + const uint64_t uploadEndMs = GetVolumeTraceSteadyMs(); + LogVolumeTraceRendering( + "UploadVolumeField end path=" + std::string(volumeField->GetPath().CStr()) + + " steady_ms=" + std::to_string(uploadEndMs) + + " create_buffer_ms=" + std::to_string(createBufferEndMs - createBufferStartMs) + + " total_ms=" + std::to_string(uploadEndMs - uploadStartMs)); cachedVolumeField.elementStride = kVolumeWordStride; cachedVolumeField.elementCount = viewDesc.elementCount; @@ -481,6 +605,147 @@ bool RenderResourceCache::UploadVolumeField( return true; } +bool RenderResourceCache::UploadGaussianSplat( + RHI::RHIDevice* device, + const Resources::GaussianSplat* gaussianSplat, + CachedGaussianSplat& cachedGaussianSplat) { + if (device == nullptr || gaussianSplat == nullptr || gaussianSplat->GetSplatCount() == 0u) { + return false; + } + + cachedGaussianSplat.residencyState = GaussianSplatResidencyState::GpuUploading; + cachedGaussianSplat.contentVersion = gaussianSplat->GetContentVersion(); + cachedGaussianSplat.splatCount = gaussianSplat->GetSplatCount(); + cachedGaussianSplat.chunkCount = gaussianSplat->GetChunkCount(); + cachedGaussianSplat.cameraCount = gaussianSplat->GetCameraCount(); + cachedGaussianSplat.bounds = gaussianSplat->GetBounds(); + + if (!UploadGaussianSplatSection( + device, + gaussianSplat, + Resources::GaussianSplatSectionType::Positions, + Resources::GaussianSplatSectionFormat::VectorFloat32, + sizeof(Resources::GaussianSplatPositionRecord), + true, + cachedGaussianSplat.positions) || + !UploadGaussianSplatSection( + device, + gaussianSplat, + Resources::GaussianSplatSectionType::Other, + Resources::GaussianSplatSectionFormat::OtherFloat32, + sizeof(Resources::GaussianSplatOtherRecord), + true, + cachedGaussianSplat.other) || + !UploadGaussianSplatSection( + device, + gaussianSplat, + Resources::GaussianSplatSectionType::Color, + Resources::GaussianSplatSectionFormat::ColorRGBA32F, + sizeof(Resources::GaussianSplatColorRecord), + true, + cachedGaussianSplat.color) || + !UploadGaussianSplatSection( + device, + gaussianSplat, + Resources::GaussianSplatSectionType::SH, + Resources::GaussianSplatSectionFormat::SHFloat32, + sizeof(Resources::GaussianSplatSHRecord), + true, + cachedGaussianSplat.sh) || + !UploadGaussianSplatSection( + device, + gaussianSplat, + Resources::GaussianSplatSectionType::Chunks, + Resources::GaussianSplatSectionFormat::ChunkFloat32, + 0u, + gaussianSplat->GetChunkCount() > 0u, + cachedGaussianSplat.chunks)) { + return false; + } + + cachedGaussianSplat.residencyState = GaussianSplatResidencyState::GpuReady; + return true; +} + +bool RenderResourceCache::UploadGaussianSplatSection( + RHI::RHIDevice* device, + const Resources::GaussianSplat* gaussianSplat, + Resources::GaussianSplatSectionType sectionType, + Resources::GaussianSplatSectionFormat requiredFormat, + uint32_t requiredStride, + bool requiredSection, + CachedGaussianSplatSection& cachedSection) { + if (device == nullptr || gaussianSplat == nullptr) { + return false; + } + + const Resources::GaussianSplatSection* section = gaussianSplat->FindSection(sectionType); + if (section == nullptr || section->dataSize == 0u || section->elementCount == 0u) { + if (requiredSection) { + return false; + } + + cachedSection.type = sectionType; + cachedSection.format = section != nullptr ? section->format : requiredFormat; + return true; + } + + const uint32_t expectedElementCount = + sectionType == Resources::GaussianSplatSectionType::Chunks + ? gaussianSplat->GetChunkCount() + : gaussianSplat->GetSplatCount(); + const uint32_t resolvedStride = + requiredStride != 0u ? requiredStride : section->elementStride; + if (!ValidateGaussianSplatSectionLayout(*section, requiredFormat, resolvedStride, expectedElementCount)) { + return false; + } + + const void* sectionData = gaussianSplat->GetSectionData(sectionType); + if (sectionData == nullptr) { + return false; + } + + if (section->dataSize > static_cast(std::numeric_limits::max())) { + return false; + } + + RHI::BufferDesc bufferDesc = {}; + bufferDesc.size = section->dataSize; + bufferDesc.stride = resolvedStride; + bufferDesc.bufferType = static_cast(RHI::BufferType::Storage); + bufferDesc.flags = 0u; + + cachedSection.buffer = device->CreateBuffer( + bufferDesc, + sectionData, + static_cast(section->dataSize), + RHI::ResourceStates::GenericRead); + if (cachedSection.buffer == nullptr) { + return false; + } + + cachedSection.buffer->SetStride(resolvedStride); + cachedSection.buffer->SetBufferType(RHI::BufferType::Storage); + + RHI::ResourceViewDesc viewDesc = {}; + viewDesc.dimension = RHI::ResourceViewDimension::StructuredBuffer; + viewDesc.firstElement = 0u; + viewDesc.elementCount = section->elementCount; + viewDesc.structureByteStride = resolvedStride; + + cachedSection.shaderResourceView = device->CreateShaderResourceView(cachedSection.buffer, viewDesc); + if (cachedSection.shaderResourceView == nullptr) { + return false; + } + + cachedSection.type = sectionType; + cachedSection.format = section->format; + cachedSection.elementStride = resolvedStride; + cachedSection.elementCount = section->elementCount; + cachedSection.payloadSize = section->dataSize; + return true; +} + bool RenderResourceCache::CreateBufferView( RHI::RHIDevice* device, RHI::RHIBuffer* buffer, diff --git a/tests/Rendering/unit/CMakeLists.txt b/tests/Rendering/unit/CMakeLists.txt index 536b84b9..f0e259b3 100644 --- a/tests/Rendering/unit/CMakeLists.txt +++ b/tests/Rendering/unit/CMakeLists.txt @@ -11,6 +11,7 @@ set(RENDERING_UNIT_TEST_SOURCES test_scene_render_request_utils.cpp test_render_scene_utility.cpp test_render_scene_extractor.cpp + test_render_resource_cache.cpp ) add_executable(rendering_unit_tests ${RENDERING_UNIT_TEST_SOURCES}) @@ -30,6 +31,7 @@ target_link_libraries(rendering_unit_tests PRIVATE target_include_directories(rendering_unit_tests PRIVATE ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/engine/src ) include(GoogleTest) diff --git a/tests/Rendering/unit/test_render_resource_cache.cpp b/tests/Rendering/unit/test_render_resource_cache.cpp new file mode 100644 index 00000000..2c35d3ed --- /dev/null +++ b/tests/Rendering/unit/test_render_resource_cache.cpp @@ -0,0 +1,538 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace XCEngine::Math; +using namespace XCEngine::Rendering; +using namespace XCEngine::Resources; + +namespace { + +struct SyntheticGaussianSplatVertex { + Vector3 position = Vector3::Zero(); + Vector3 dc0 = Vector3::Zero(); + float opacity = 0.0f; + Vector3 scaleLog = Vector3::Zero(); + float rotationWXYZ[4] = { 1.0f, 0.0f, 0.0f, 0.0f }; + float sh[kGaussianSplatSHCoefficientCount] = {}; +}; + +struct SampleArtifactData { + GaussianSplatMetadata metadata; + XCEngine::Containers::Array sections; + XCEngine::Containers::Array payload; +}; + +struct MockCacheAllocationState { + int createBufferCalls = 0; + int shutdownBufferCalls = 0; + int destroyBufferCalls = 0; + int createShaderViewCalls = 0; + int shutdownShaderViewCalls = 0; + int destroyShaderViewCalls = 0; + std::vector bufferDescs; + std::vector viewDescs; +}; + +class MockCacheBuffer final : public XCEngine::RHI::RHIBuffer { +public: + MockCacheBuffer(std::shared_ptr state, const XCEngine::RHI::BufferDesc& desc) + : m_state(std::move(state)) + , m_size(desc.size) + , m_stride(desc.stride) + , m_bufferType(static_cast(desc.bufferType)) + , m_data(static_cast(desc.size), 0u) { + } + + ~MockCacheBuffer() override { + ++m_state->destroyBufferCalls; + } + + void* Map() override { + return m_data.empty() ? nullptr : m_data.data(); + } + + void Unmap() override {} + + void SetData(const void* data, size_t size, size_t offset = 0) override { + ASSERT_NE(data, nullptr); + ASSERT_LE(offset + size, m_data.size()); + std::memcpy(m_data.data() + offset, data, size); + } + + uint64_t GetSize() const override { return m_size; } + XCEngine::RHI::BufferType GetBufferType() const override { return m_bufferType; } + void SetBufferType(XCEngine::RHI::BufferType type) override { m_bufferType = 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_stateValue; } + void SetState(XCEngine::RHI::ResourceStates state) override { m_stateValue = 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->shutdownBufferCalls; + } + + const std::vector& GetBytes() const { return m_data; } + +private: + std::shared_ptr m_state; + uint64_t m_size = 0u; + uint32_t m_stride = 0u; + XCEngine::RHI::BufferType m_bufferType = XCEngine::RHI::BufferType::Storage; + XCEngine::RHI::ResourceStates m_stateValue = XCEngine::RHI::ResourceStates::Common; + std::string m_name; + std::vector m_data; +}; + +class MockCacheView final : public XCEngine::RHI::RHIResourceView { +public: + MockCacheView( + std::shared_ptr state, + XCEngine::RHI::ResourceViewType viewType, + const XCEngine::RHI::ResourceViewDesc& desc) + : m_state(std::move(state)) + , m_viewType(viewType) + , m_dimension(desc.dimension) + , m_format(static_cast(desc.format)) { + } + + ~MockCacheView() override { + ++m_state->destroyShaderViewCalls; + } + + void Shutdown() override { + ++m_state->shutdownShaderViewCalls; + } + + 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: + std::shared_ptr m_state; + XCEngine::RHI::ResourceViewType m_viewType = XCEngine::RHI::ResourceViewType::ShaderResource; + XCEngine::RHI::ResourceViewDimension m_dimension = XCEngine::RHI::ResourceViewDimension::Unknown; + XCEngine::RHI::Format m_format = XCEngine::RHI::Format::Unknown; +}; + +class MockCacheDevice final : public XCEngine::RHI::RHIDevice { +public: + explicit MockCacheDevice(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; + m_state->bufferDescs.push_back(desc); + return new MockCacheBuffer(m_state, desc); + } + + XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&) override { return nullptr; } + XCEngine::RHI::RHITexture* CreateTexture( + const XCEngine::RHI::TextureDesc&, + const void*, + size_t, + uint32_t) override { return nullptr; } + 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&) override { return nullptr; } + 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->createShaderViewCalls; + m_state->viewDescs.push_back(desc); + return new MockCacheView(m_state, XCEngine::RHI::ResourceViewType::ShaderResource, desc); + } + + XCEngine::RHI::RHIResourceView* CreateShaderResourceView( + XCEngine::RHI::RHITexture*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } + XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView( + XCEngine::RHI::RHIBuffer*, + const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } + 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_deviceInfo; } + void* GetNativeDevice() override { return nullptr; } + +private: + std::shared_ptr m_state; + XCEngine::RHI::RHICapabilities m_capabilities = {}; + XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {}; +}; + +void WriteBinaryFloat(std::ofstream& output, float value) { + output.write(reinterpret_cast(&value), sizeof(value)); +} + +void WriteSyntheticGaussianSplatPly( + const std::filesystem::path& path, + const std::vector& vertices) { + std::ofstream output(path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(output.is_open()); + + output << "ply\n"; + output << "format binary_little_endian 1.0\n"; + output << "element vertex " << vertices.size() << "\n"; + output << "property float opacity\n"; + output << "property float y\n"; + output << "property float scale_2\n"; + output << "property float rot_3\n"; + output << "property float f_dc_1\n"; + output << "property float x\n"; + output << "property float scale_0\n"; + output << "property float rot_1\n"; + output << "property float f_dc_2\n"; + output << "property float z\n"; + output << "property float scale_1\n"; + output << "property float rot_0\n"; + output << "property float f_dc_0\n"; + output << "property float rot_2\n"; + for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) { + output << "property float f_rest_" << index << "\n"; + } + output << "end_header\n"; + + for (const SyntheticGaussianSplatVertex& vertex : vertices) { + WriteBinaryFloat(output, vertex.opacity); + WriteBinaryFloat(output, vertex.position.y); + WriteBinaryFloat(output, vertex.scaleLog.z); + WriteBinaryFloat(output, vertex.rotationWXYZ[3]); + WriteBinaryFloat(output, vertex.dc0.y); + WriteBinaryFloat(output, vertex.position.x); + WriteBinaryFloat(output, vertex.scaleLog.x); + WriteBinaryFloat(output, vertex.rotationWXYZ[1]); + WriteBinaryFloat(output, vertex.dc0.z); + WriteBinaryFloat(output, vertex.position.z); + WriteBinaryFloat(output, vertex.scaleLog.y); + WriteBinaryFloat(output, vertex.rotationWXYZ[0]); + WriteBinaryFloat(output, vertex.dc0.x); + WriteBinaryFloat(output, vertex.rotationWXYZ[2]); + for (float coefficient : vertex.sh) { + WriteBinaryFloat(output, coefficient); + } + } +} + +SampleArtifactData BuildSampleArtifactData() { + const GaussianSplatPositionRecord positions[2] = { + { Vector3(0.0f, 1.0f, 2.0f) }, + { Vector3(3.0f, 4.0f, 5.0f) } + }; + const GaussianSplatOtherRecord other[2] = { + { Quaternion::Identity(), Vector3(1.0f, 1.0f, 1.0f), 0.0f }, + { Quaternion(0.0f, 0.5f, 0.0f, 0.8660254f), Vector3(2.0f, 2.0f, 2.0f), 0.0f } + }; + const GaussianSplatColorRecord colors[2] = { + { Vector4(1.0f, 0.0f, 0.0f, 0.25f) }, + { Vector4(0.0f, 1.0f, 0.0f, 0.75f) } + }; + + GaussianSplatSHRecord sh[2]; + for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) { + sh[0].coefficients[index] = 0.01f * static_cast(index + 1u); + sh[1].coefficients[index] = -0.02f * static_cast(index + 1u); + } + + SampleArtifactData sample; + sample.metadata.contentVersion = 1u; + sample.metadata.splatCount = 2u; + sample.metadata.bounds.SetMinMax(Vector3(-2.0f, -1.0f, -3.0f), Vector3(5.0f, 4.0f, 6.0f)); + sample.metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32; + sample.metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32; + sample.metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F; + sample.metadata.shFormat = GaussianSplatSectionFormat::SHFloat32; + + sample.sections.Reserve(4u); + size_t payloadOffset = 0u; + auto appendSection = [&](GaussianSplatSectionType type, + GaussianSplatSectionFormat format, + const void* data, + size_t dataSize, + XCEngine::Core::uint32 elementCount, + XCEngine::Core::uint32 elementStride) { + GaussianSplatSection section; + section.type = type; + section.format = format; + section.dataOffset = payloadOffset; + section.dataSize = dataSize; + section.elementCount = elementCount; + section.elementStride = elementStride; + sample.sections.PushBack(section); + + const size_t newPayloadSize = sample.payload.Size() + dataSize; + sample.payload.Resize(newPayloadSize); + std::memcpy(sample.payload.Data() + payloadOffset, data, dataSize); + payloadOffset = newPayloadSize; + }; + + appendSection( + GaussianSplatSectionType::Positions, + GaussianSplatSectionFormat::VectorFloat32, + positions, + sizeof(positions), + 2u, + sizeof(GaussianSplatPositionRecord)); + appendSection( + GaussianSplatSectionType::Other, + GaussianSplatSectionFormat::OtherFloat32, + other, + sizeof(other), + 2u, + sizeof(GaussianSplatOtherRecord)); + appendSection( + GaussianSplatSectionType::Color, + GaussianSplatSectionFormat::ColorRGBA32F, + colors, + sizeof(colors), + 2u, + sizeof(GaussianSplatColorRecord)); + appendSection( + GaussianSplatSectionType::SH, + GaussianSplatSectionFormat::SHFloat32, + sh, + sizeof(sh), + 2u, + sizeof(GaussianSplatSHRecord)); + + return sample; +} + +GaussianSplat BuildSampleGaussianSplat(const char* artifactPath) { + SampleArtifactData sample = BuildSampleArtifactData(); + + GaussianSplat gaussianSplat; + XCEngine::Resources::IResource::ConstructParams params; + params.name = "sample.xcgsplat"; + params.path = artifactPath; + params.guid = ResourceGUID::Generate(params.path); + gaussianSplat.Initialize(params); + EXPECT_TRUE(gaussianSplat.CreateOwned( + sample.metadata, + std::move(sample.sections), + std::move(sample.payload))); + return gaussianSplat; +} + +std::filesystem::path CreateTestProjectRoot(const char* folderName) { + return std::filesystem::current_path() / "__xc_gaussian_splat_cache_test_runtime" / folderName; +} + +TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatUploadsStructuredSectionsAndReusesEntry) { + GaussianSplat gaussianSplat = BuildSampleGaussianSplat("sample.xcgsplat"); + + auto state = std::make_shared(); + MockCacheDevice device(state); + RenderResourceCache cache; + + const RenderResourceCache::CachedGaussianSplat* cached = + cache.GetOrCreateGaussianSplat(&device, &gaussianSplat); + ASSERT_NE(cached, nullptr); + EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady); + EXPECT_EQ(cached->contentVersion, 1u); + EXPECT_EQ(cached->splatCount, 2u); + EXPECT_EQ(cached->chunkCount, 0u); + EXPECT_EQ(cached->positions.elementStride, sizeof(GaussianSplatPositionRecord)); + EXPECT_EQ(cached->other.elementStride, sizeof(GaussianSplatOtherRecord)); + EXPECT_EQ(cached->color.elementStride, sizeof(GaussianSplatColorRecord)); + EXPECT_EQ(cached->sh.elementStride, sizeof(GaussianSplatSHRecord)); + EXPECT_EQ(cached->positions.elementCount, 2u); + EXPECT_EQ(cached->other.elementCount, 2u); + EXPECT_EQ(cached->color.elementCount, 2u); + EXPECT_EQ(cached->sh.elementCount, 2u); + ASSERT_NE(cached->positions.buffer, nullptr); + ASSERT_NE(cached->positions.shaderResourceView, nullptr); + ASSERT_NE(cached->color.buffer, nullptr); + ASSERT_NE(cached->sh.buffer, nullptr); + + const auto* uploadedPositions = static_cast(cached->positions.buffer); + ASSERT_GE(uploadedPositions->GetBytes().size(), sizeof(GaussianSplatPositionRecord) * 2u); + const auto* uploadedPositionRecords = reinterpret_cast( + uploadedPositions->GetBytes().data()); + EXPECT_EQ(uploadedPositionRecords[0].position, Vector3(0.0f, 1.0f, 2.0f)); + EXPECT_EQ(uploadedPositionRecords[1].position, Vector3(3.0f, 4.0f, 5.0f)); + + EXPECT_EQ(state->createBufferCalls, 4); + EXPECT_EQ(state->createShaderViewCalls, 4); + + const RenderResourceCache::CachedGaussianSplat* cachedAgain = + cache.GetOrCreateGaussianSplat(&device, &gaussianSplat); + EXPECT_EQ(cachedAgain, cached); + EXPECT_EQ(state->createBufferCalls, 4); + EXPECT_EQ(state->createShaderViewCalls, 4); + + cache.Shutdown(); + EXPECT_EQ(state->shutdownBufferCalls, 4); + EXPECT_EQ(state->destroyBufferCalls, 4); + EXPECT_EQ(state->shutdownShaderViewCalls, 4); + EXPECT_EQ(state->destroyShaderViewCalls, 4); +} + +TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatSupportsArtifactRuntimeLoadPath) { + namespace fs = std::filesystem; + + const fs::path tempDir = fs::temp_directory_path() / "xc_gaussian_splat_render_cache_artifact"; + const fs::path artifactPath = tempDir / "sample.xcgsplat"; + + fs::remove_all(tempDir); + fs::create_directories(tempDir); + + const GaussianSplat source = BuildSampleGaussianSplat(artifactPath.string().c_str()); + XCEngine::Containers::String errorMessage; + ASSERT_TRUE(WriteGaussianSplatArtifactFile(artifactPath.string().c_str(), source, &errorMessage)) + << errorMessage.CStr(); + + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + { + const auto handle = manager.Load(artifactPath.string().c_str()); + ASSERT_TRUE(handle.IsValid()); + + auto state = std::make_shared(); + MockCacheDevice device(state); + RenderResourceCache cache; + + const RenderResourceCache::CachedGaussianSplat* cached = + cache.GetOrCreateGaussianSplat(&device, handle.Get()); + ASSERT_NE(cached, nullptr); + EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady); + EXPECT_EQ(cached->splatCount, 2u); + EXPECT_EQ(state->createBufferCalls, 4); + EXPECT_EQ(state->createShaderViewCalls, 4); + } + + manager.UnloadAll(); + manager.Shutdown(); + fs::remove_all(tempDir); +} + +TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatSupportsSourceAssetImportPath) { + namespace fs = std::filesystem; + + const fs::path projectRoot = CreateTestProjectRoot("source_asset_import"); + const fs::path assetsDir = projectRoot / "Assets"; + const fs::path sourcePath = assetsDir / "sample.ply"; + + fs::remove_all(projectRoot); + fs::create_directories(assetsDir); + + std::vector vertices(2); + vertices[0].position = Vector3(1.0f, 2.0f, 3.0f); + vertices[0].dc0 = Vector3(0.2f, -0.1f, 0.0f); + vertices[0].opacity = 0.25f; + vertices[0].scaleLog = Vector3(0.0f, std::log(2.0f), std::log(4.0f)); + vertices[0].rotationWXYZ[0] = 1.0f; + for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) { + vertices[0].sh[index] = 0.01f * static_cast(index + 1u); + } + + vertices[1].position = Vector3(-4.0f, -5.0f, -6.0f); + vertices[1].dc0 = Vector3(1.0f, 0.5f, -0.5f); + vertices[1].opacity = -1.0f; + vertices[1].scaleLog = Vector3(std::log(0.5f), 0.0f, std::log(3.0f)); + vertices[1].rotationWXYZ[2] = 3.0f; + vertices[1].rotationWXYZ[3] = 4.0f; + for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) { + vertices[1].sh[index] = -0.02f * static_cast(index + 1u); + } + + WriteSyntheticGaussianSplatPly(sourcePath, vertices); + + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + manager.SetResourceRoot(projectRoot.string().c_str()); + + { + const auto handle = manager.Load("Assets/sample.ply"); + ASSERT_TRUE(handle.IsValid()); + EXPECT_EQ(handle->GetSplatCount(), 2u); + + auto state = std::make_shared(); + MockCacheDevice device(state); + RenderResourceCache cache; + + const RenderResourceCache::CachedGaussianSplat* cached = + cache.GetOrCreateGaussianSplat(&device, handle.Get()); + ASSERT_NE(cached, nullptr); + EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady); + EXPECT_EQ(cached->splatCount, 2u); + EXPECT_EQ(state->createBufferCalls, 4); + EXPECT_EQ(state->createShaderViewCalls, 4); + + const auto handleAgain = manager.Load("Assets/sample.ply"); + ASSERT_TRUE(handleAgain.IsValid()); + const RenderResourceCache::CachedGaussianSplat* cachedAgain = + cache.GetOrCreateGaussianSplat(&device, handleAgain.Get()); + EXPECT_EQ(cachedAgain, cached); + EXPECT_EQ(state->createBufferCalls, 4); + EXPECT_EQ(state->createShaderViewCalls, 4); + } + + manager.UnloadAll(); + manager.Shutdown(); + fs::remove_all(projectRoot); +} + +} // namespace