#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); } const GaussianSplatChunkRecord chunks[1] = { { 0x00010002u, 0x00030004u, 0x00050006u, 0x00070008u, Vector2(-1.0f, 2.0f), Vector2(-3.0f, 4.0f), Vector2(-5.0f, 6.0f), 0x0009000Au, 0x000B000Cu, 0x000D000Eu, 0x000F0010u, 0x00110012u, 0x00130014u } }; SampleArtifactData sample; sample.metadata.contentVersion = 1u; sample.metadata.splatCount = 2u; sample.metadata.chunkCount = 1u; 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.metadata.chunkFormat = GaussianSplatSectionFormat::ChunkFloat32; sample.sections.Reserve(5u); 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)); appendSection( GaussianSplatSectionType::Chunks, GaussianSplatSectionFormat::ChunkFloat32, chunks, sizeof(chunks), 1u, sizeof(GaussianSplatChunkRecord)); 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, 1u); EXPECT_EQ(cached->positions.elementStride, sizeof(float) * 4u); EXPECT_EQ(cached->other.elementStride, sizeof(GaussianSplatOtherRecord)); EXPECT_EQ(cached->color.elementStride, sizeof(GaussianSplatColorRecord)); EXPECT_EQ(cached->sh.elementStride, sizeof(GaussianSplatSHRecord)); EXPECT_EQ(cached->chunks.elementStride, sizeof(GaussianSplatChunkRecord)); EXPECT_EQ(cached->positions.elementCount, 2u); EXPECT_EQ(cached->other.elementCount, 2u); EXPECT_EQ(cached->color.elementCount, 2u); EXPECT_EQ(cached->sh.elementCount, 2u); EXPECT_EQ(cached->chunks.elementCount, 1u); ASSERT_NE(cached->positions.buffer, nullptr); ASSERT_NE(cached->positions.shaderResourceView, nullptr); ASSERT_NE(cached->color.buffer, nullptr); ASSERT_NE(cached->sh.buffer, nullptr); ASSERT_NE(cached->chunks.buffer, nullptr); const auto* uploadedPositions = static_cast(cached->positions.buffer); ASSERT_GE(uploadedPositions->GetBytes().size(), sizeof(float) * 4u * 2u); const auto* uploadedPositionWords = reinterpret_cast(uploadedPositions->GetBytes().data()); EXPECT_FLOAT_EQ(uploadedPositionWords[0], 0.0f); EXPECT_FLOAT_EQ(uploadedPositionWords[1], 1.0f); EXPECT_FLOAT_EQ(uploadedPositionWords[2], 2.0f); EXPECT_FLOAT_EQ(uploadedPositionWords[3], 0.0f); EXPECT_FLOAT_EQ(uploadedPositionWords[4], 3.0f); EXPECT_FLOAT_EQ(uploadedPositionWords[5], 4.0f); EXPECT_FLOAT_EQ(uploadedPositionWords[6], 5.0f); EXPECT_FLOAT_EQ(uploadedPositionWords[7], 0.0f); EXPECT_EQ(state->createBufferCalls, 5); EXPECT_EQ(state->createShaderViewCalls, 5); const RenderResourceCache::CachedGaussianSplat* cachedAgain = cache.GetOrCreateGaussianSplat(&device, &gaussianSplat); EXPECT_EQ(cachedAgain, cached); EXPECT_EQ(state->createBufferCalls, 5); EXPECT_EQ(state->createShaderViewCalls, 5); cache.Shutdown(); EXPECT_EQ(state->shutdownBufferCalls, 5); EXPECT_EQ(state->destroyBufferCalls, 5); EXPECT_EQ(state->shutdownShaderViewCalls, 5); EXPECT_EQ(state->destroyShaderViewCalls, 5); } 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(cached->chunkCount, 1u); EXPECT_EQ(state->createBufferCalls, 5); EXPECT_EQ(state->createShaderViewCalls, 5); } 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