#include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Resources; namespace { GaussianSplat* CreateTestGaussianSplat(const char* name, const char* path) { auto* gaussianSplat = new GaussianSplat(); IResource::ConstructParams params = {}; params.name = name; params.path = path; params.guid = ResourceGUID::Generate(path); gaussianSplat->Initialize(params); GaussianSplatMetadata metadata = {}; metadata.splatCount = 1u; XCEngine::Containers::Array sections; sections.Resize(1); sections[0].type = GaussianSplatSectionType::Positions; sections[0].format = GaussianSplatSectionFormat::VectorFloat32; sections[0].dataOffset = 0u; sections[0].dataSize = sizeof(GaussianSplatPositionRecord); sections[0].elementCount = 1u; sections[0].elementStride = sizeof(GaussianSplatPositionRecord); XCEngine::Containers::Array payload; payload.Resize(sizeof(GaussianSplatPositionRecord)); const GaussianSplatPositionRecord positionRecord = { XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f) }; std::memcpy(payload.Data(), &positionRecord, sizeof(positionRecord)); EXPECT_TRUE(gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))); return gaussianSplat; } Material* CreateTestMaterial(const char* name, const char* path) { auto* material = new Material(); IResource::ConstructParams params = {}; params.name = name; params.path = path; params.guid = ResourceGUID::Generate(path); material->Initialize(params); return material; } class FakeAsyncGaussianSplatLoader final : public IResourceLoader { public: ResourceType GetResourceType() const override { return ResourceType::GaussianSplat; } XCEngine::Containers::Array GetSupportedExtensions() const override { XCEngine::Containers::Array extensions; extensions.PushBack("ply"); extensions.PushBack("xcgsplat"); return extensions; } bool CanLoad(const XCEngine::Containers::String& path) const override { (void)path; return true; } LoadResult Load( const XCEngine::Containers::String& path, const ImportSettings* settings = nullptr) override { (void)settings; return LoadResult(CreateTestGaussianSplat("AsyncGaussianSplat", path.CStr())); } ImportSettings* GetDefaultSettings() const override { return nullptr; } }; bool PumpAsyncLoadsUntilIdle( ResourceManager& manager, std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) { const auto deadline = std::chrono::steady_clock::now() + timeout; while (manager.IsAsyncLoading() && std::chrono::steady_clock::now() < deadline) { manager.UpdateAsyncLoads(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } manager.UpdateAsyncLoads(); return !manager.IsAsyncLoading(); } TEST(GaussianSplatRendererComponent_Test, SetResourcesCachesHandlesPathsAndFlags) { GameObject gameObject("GaussianSplatHolder"); auto* component = gameObject.AddComponent(); GaussianSplat* gaussianSplat = CreateTestGaussianSplat("Room", "GaussianSplats/room.xcgsplat"); Material* material = CreateTestMaterial("GaussianSplatMaterial", "Materials/gaussian_splat.mat"); component->SetGaussianSplat(gaussianSplat); component->SetMaterial(material); component->SetCastShadows(false); component->SetReceiveShadows(false); EXPECT_EQ(component->GetGaussianSplat(), gaussianSplat); EXPECT_EQ(component->GetGaussianSplatPath(), "GaussianSplats/room.xcgsplat"); EXPECT_EQ(component->GetMaterial(), material); EXPECT_EQ(component->GetMaterialPath(), "Materials/gaussian_splat.mat"); EXPECT_FALSE(component->GetCastShadows()); EXPECT_FALSE(component->GetReceiveShadows()); component->ClearGaussianSplat(); component->ClearMaterial(); delete gaussianSplat; delete material; } TEST(GaussianSplatRendererComponent_Test, SerializeAndDeserializePreservesVirtualPathsAndFlags) { GaussianSplatRendererComponent source; source.SetGaussianSplatPath("test://gaussian_splats/room.ply"); source.SetMaterialPath("test://materials/gaussian_splat.mat"); source.SetCastShadows(false); source.SetReceiveShadows(true); std::stringstream stream; source.Serialize(stream); const std::string serialized = stream.str(); EXPECT_NE(serialized.find("gaussianSplatPath=test://gaussian_splats/room.ply;"), std::string::npos); EXPECT_NE(serialized.find("materialPath=test://materials/gaussian_splat.mat;"), std::string::npos); EXPECT_NE(serialized.find("castShadows=0;"), std::string::npos); EXPECT_NE(serialized.find("receiveShadows=1;"), std::string::npos); GaussianSplatRendererComponent target; std::stringstream deserializeStream(serialized); target.Deserialize(deserializeStream); EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/room.ply"); EXPECT_EQ(target.GetMaterialPath(), "test://materials/gaussian_splat.mat"); EXPECT_FALSE(target.GetCastShadows()); EXPECT_TRUE(target.GetReceiveShadows()); EXPECT_FALSE(target.GetGaussianSplatAssetRef().IsValid()); EXPECT_FALSE(target.GetMaterialAssetRef().IsValid()); } TEST(GaussianSplatRendererComponent_Test, DeferredSceneDeserializeLoadsGaussianSplatAsyncByPath) { ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); IResourceLoader* originalLoader = manager.GetLoader(ResourceType::GaussianSplat); FakeAsyncGaussianSplatLoader fakeLoader; manager.RegisterLoader(&fakeLoader); GaussianSplatRendererComponent target; const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount(); { ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled()); std::stringstream stream( "gaussianSplatPath=test://gaussian_splats/async_room.ply;gaussianSplatRef=;materialRef=;castShadows=1;receiveShadows=1;"); target.Deserialize(stream); } EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/async_room.ply"); EXPECT_EQ(target.GetGaussianSplat(), nullptr); EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager)); ASSERT_NE(target.GetGaussianSplat(), nullptr); EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/async_room.ply"); EXPECT_EQ(target.GetGaussianSplat()->GetSplatCount(), 1u); manager.RegisterLoader(originalLoader); manager.Shutdown(); } } // namespace