#include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Resources; namespace { Mesh* CreateTestMesh(const char* name, const char* path) { auto* mesh = new Mesh(); IResource::ConstructParams params = {}; params.name = name; params.path = path; params.guid = ResourceGUID::Generate(path); mesh->Initialize(params); return mesh; } 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 FakeAsyncMeshLoader : public IResourceLoader { public: ResourceType GetResourceType() const override { return ResourceType::Mesh; } XCEngine::Containers::Array GetSupportedExtensions() const override { XCEngine::Containers::Array extensions; extensions.PushBack("mesh"); 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; auto* mesh = new Mesh(); IResource::ConstructParams params = {}; params.name = "AsyncMesh"; params.path = path; params.guid = ResourceGUID::Generate(path); mesh->Initialize(params); const StaticMeshVertex vertices[3] = {}; const XCEngine::Core::uint32 indices[3] = {0, 1, 2}; mesh->SetVertexData(vertices, sizeof(vertices), 3, sizeof(StaticMeshVertex), VertexAttribute::Position); mesh->SetIndexData(indices, sizeof(indices), 3, true); return LoadResult(mesh); } 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(MeshFilterComponent_Test, SetMeshCachesResourceAndPath) { GameObject gameObject("MeshHolder"); auto* component = gameObject.AddComponent(); Mesh* mesh = CreateTestMesh("Quad", "Meshes/quad.mesh"); component->SetMesh(mesh); EXPECT_EQ(component->GetMesh(), mesh); EXPECT_EQ(component->GetMeshPath(), "Meshes/quad.mesh"); component->ClearMesh(); delete mesh; } TEST(MeshFilterComponent_Test, SerializeAndDeserializePreservesPath) { MeshFilterComponent source; source.SetMeshPath("builtin://meshes/cube"); std::stringstream stream; source.Serialize(stream); const std::string serialized = stream.str(); EXPECT_NE(serialized.find("meshRef="), std::string::npos); EXPECT_NE(serialized.find("meshPath=builtin://meshes/cube;"), std::string::npos); MeshFilterComponent target; std::stringstream deserializeStream(serialized); target.Deserialize(deserializeStream); EXPECT_EQ(target.GetMeshPath(), "builtin://meshes/cube"); ASSERT_NE(target.GetMesh(), nullptr); EXPECT_FALSE(target.GetMeshAssetRef().IsValid()); } TEST(MeshFilterComponent_Test, DeserializeIgnoresPlainMeshPathWithoutAssetRef) { MeshFilterComponent target; std::stringstream stream("meshPath=Meshes/legacy.mesh;meshRef=;"); target.Deserialize(stream); EXPECT_TRUE(target.GetMeshPath().empty()); EXPECT_EQ(target.GetMesh(), nullptr); EXPECT_FALSE(target.GetMeshAssetRef().IsValid()); } TEST(MeshFilterComponent_Test, SetMeshPathPreservesPathWithoutLoadedResource) { MeshFilterComponent component; component.SetMeshPath("Meshes/runtime.mesh"); EXPECT_EQ(component.GetMeshPath(), "Meshes/runtime.mesh"); EXPECT_EQ(component.GetMesh(), nullptr); component.SetMeshPath(""); EXPECT_EQ(component.GetMeshPath(), ""); EXPECT_EQ(component.GetMesh(), nullptr); } TEST(MeshFilterComponent_Test, SetMeshAssetRefPreservesProjectSubAssetReference) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_filter_asset_ref_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path meshPath = assetsDir / "runtime.mesh"; fs::remove_all(projectRoot); fs::create_directories(assetsDir); { std::ofstream meshFile(meshPath); ASSERT_TRUE(meshFile.is_open()); meshFile << "placeholder"; } manager.SetResourceRoot(projectRoot.string().c_str()); AssetRef meshRef; ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.mesh", ResourceType::Mesh, meshRef)); MeshFilterComponent component; component.SetMeshAssetRef(meshRef); EXPECT_TRUE(component.GetMeshAssetRef().IsValid()); EXPECT_EQ(component.GetMeshAssetRef().assetGuid, meshRef.assetGuid); EXPECT_EQ(component.GetMeshAssetRef().localID, meshRef.localID); EXPECT_EQ(component.GetMeshAssetRef().resourceType, meshRef.resourceType); EXPECT_EQ(component.GetMeshPath(), "Assets/runtime.mesh"); std::stringstream stream; component.Serialize(stream); EXPECT_NE(stream.str().find("meshRef="), std::string::npos); EXPECT_EQ(stream.str().find("meshRef=;"), std::string::npos); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) { ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); IResourceLoader* originalLoader = manager.GetLoader(ResourceType::Mesh); FakeAsyncMeshLoader fakeLoader; manager.RegisterLoader(&fakeLoader); MeshFilterComponent target; const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount(); { ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled()); std::stringstream stream("meshPath=test://meshes/async.mesh;meshRef=;"); target.Deserialize(stream); } EXPECT_EQ(target.GetMeshPath(), "test://meshes/async.mesh"); EXPECT_EQ(target.GetMesh(), nullptr); EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager)); ASSERT_NE(target.GetMesh(), nullptr); EXPECT_EQ(target.GetMeshPath(), "test://meshes/async.mesh"); EXPECT_EQ(target.GetMesh()->GetVertexCount(), 3u); manager.RegisterLoader(originalLoader); manager.Shutdown(); } TEST(MeshRendererComponent_Test, SetMaterialsKeepsSlotsAndFlags) { GameObject gameObject("RendererHolder"); auto* component = gameObject.AddComponent(); Material* material0 = CreateTestMaterial("M0", "Materials/m0.mat"); Material* material1 = CreateTestMaterial("M1", "Materials/m1.mat"); component->SetMaterial(0, material0); component->SetMaterial(1, material1); component->SetCastShadows(false); component->SetReceiveShadows(false); component->SetRenderLayer(7); ASSERT_EQ(component->GetMaterialCount(), 2u); EXPECT_EQ(component->GetMaterial(0), material0); EXPECT_EQ(component->GetMaterial(1), material1); EXPECT_EQ(component->GetMaterialPaths()[0], "Materials/m0.mat"); EXPECT_EQ(component->GetMaterialPaths()[1], "Materials/m1.mat"); EXPECT_FALSE(component->GetCastShadows()); EXPECT_FALSE(component->GetReceiveShadows()); EXPECT_EQ(component->GetRenderLayer(), 7u); component->ClearMaterials(); delete material0; delete material1; } TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesMaterialPathsAndSettings) { MeshRendererComponent source; source.SetMaterialPath(0, "builtin://materials/default-primitive"); source.SetMaterialPath(1, "builtin://materials/default-primitive"); source.SetCastShadows(false); source.SetReceiveShadows(true); source.SetRenderLayer(3); std::stringstream stream; source.Serialize(stream); const std::string serialized = stream.str(); EXPECT_NE( serialized.find("materialPaths=builtin://materials/default-primitive|builtin://materials/default-primitive;"), std::string::npos); EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos); MeshRendererComponent target; std::stringstream deserializeStream(serialized); target.Deserialize(deserializeStream); ASSERT_EQ(target.GetMaterialCount(), 2u); ASSERT_NE(target.GetMaterial(0), nullptr); ASSERT_NE(target.GetMaterial(1), nullptr); EXPECT_EQ(target.GetMaterialPaths()[0], "builtin://materials/default-primitive"); EXPECT_EQ(target.GetMaterialPaths()[1], "builtin://materials/default-primitive"); EXPECT_FALSE(target.GetCastShadows()); EXPECT_TRUE(target.GetReceiveShadows()); EXPECT_EQ(target.GetRenderLayer(), 3u); } TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesTrailingEmptyMaterialSlots) { MeshRendererComponent source; source.SetMaterialPath(0, "builtin://materials/default-primitive"); source.SetMaterialPath(1, ""); source.SetCastShadows(false); source.SetReceiveShadows(true); source.SetRenderLayer(9); std::stringstream stream; source.Serialize(stream); const std::string serialized = stream.str(); EXPECT_NE(serialized.find("materialPaths=builtin://materials/default-primitive|;"), std::string::npos); EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos); MeshRendererComponent target; std::stringstream deserializeStream(serialized); target.Deserialize(deserializeStream); ASSERT_EQ(target.GetMaterialCount(), 2u); ASSERT_NE(target.GetMaterial(0), nullptr); EXPECT_EQ(target.GetMaterial(1), nullptr); EXPECT_EQ(target.GetMaterialPath(0), "builtin://materials/default-primitive"); EXPECT_EQ(target.GetMaterialPath(1), ""); EXPECT_FALSE(target.GetCastShadows()); EXPECT_TRUE(target.GetReceiveShadows()); EXPECT_EQ(target.GetRenderLayer(), 9u); } TEST(MeshRendererComponent_Test, SetMaterialPathPreservesPathWithoutLoadedResource) { MeshRendererComponent component; component.SetMaterialPath(1, "Materials/runtime.mat"); ASSERT_EQ(component.GetMaterialCount(), 2u); EXPECT_EQ(component.GetMaterial(0), nullptr); EXPECT_EQ(component.GetMaterial(1), nullptr); EXPECT_EQ(component.GetMaterialPath(0), ""); EXPECT_EQ(component.GetMaterialPath(1), "Materials/runtime.mat"); component.SetMaterialPath(1, ""); EXPECT_EQ(component.GetMaterialPath(1), ""); EXPECT_EQ(component.GetMaterial(1), nullptr); } TEST(MeshRendererComponent_Test, SetMaterialAssetRefPreservesProjectSubAssetReference) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_renderer_sub_asset_ref_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path materialPath = assetsDir / "runtime.material"; fs::remove_all(projectRoot); fs::create_directories(assetsDir); { std::ofstream materialFile(materialPath); ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; materialFile << " \"renderQueue\": \"geometry\",\n"; materialFile << " \"renderState\": {\n"; materialFile << " \"cull\": \"back\"\n"; materialFile << " }\n"; materialFile << "}"; } manager.SetResourceRoot(projectRoot.string().c_str()); AssetRef materialRef; ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, materialRef)); MeshRendererComponent component; component.SetMaterialAssetRef(0, materialRef); ASSERT_EQ(component.GetMaterialCount(), 1u); EXPECT_TRUE(component.GetMaterialAssetRefs()[0].IsValid()); EXPECT_EQ(component.GetMaterialAssetRefs()[0].assetGuid, materialRef.assetGuid); EXPECT_EQ(component.GetMaterialAssetRefs()[0].localID, materialRef.localID); EXPECT_EQ(component.GetMaterialAssetRefs()[0].resourceType, materialRef.resourceType); EXPECT_EQ(component.GetMaterialPath(0), "Assets/runtime.material"); ASSERT_NE(component.GetMaterial(0), nullptr); std::stringstream stream; component.Serialize(stream); EXPECT_NE(stream.str().find("materialRefs="), std::string::npos); EXPECT_EQ(stream.str().find("materialRefs=;"), std::string::npos); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(MeshRendererComponent_Test, DeserializeIgnoresPlainMaterialPathsWithoutAssetRefs) { MeshRendererComponent target; std::stringstream stream( "materialPaths=Materials/legacy0.mat|;materialRefs=|;castShadows=0;receiveShadows=1;renderLayer=5;"); target.Deserialize(stream); ASSERT_EQ(target.GetMaterialCount(), 2u); EXPECT_EQ(target.GetMaterialPath(0), ""); EXPECT_EQ(target.GetMaterialPath(1), ""); EXPECT_EQ(target.GetMaterial(0), nullptr); EXPECT_EQ(target.GetMaterial(1), nullptr); EXPECT_FALSE(target.GetCastShadows()); EXPECT_TRUE(target.GetReceiveShadows()); EXPECT_EQ(target.GetRenderLayer(), 5u); } TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAssetRef) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_renderer_asset_ref_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path materialPath = assetsDir / "runtime.material"; fs::remove_all(projectRoot); fs::create_directories(assetsDir); { std::ofstream materialFile(materialPath); ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; materialFile << " \"renderQueue\": \"geometry\",\n"; materialFile << " \"renderState\": {\n"; materialFile << " \"cull\": \"back\"\n"; materialFile << " }\n"; materialFile << "}"; } manager.SetResourceRoot(projectRoot.string().c_str()); MeshRendererComponent source; source.SetMaterialPath(0, "Assets/runtime.material"); ASSERT_EQ(source.GetMaterialCount(), 1u); ASSERT_NE(source.GetMaterial(0), nullptr); ASSERT_EQ(source.GetMaterialPath(0), "Assets/runtime.material"); ASSERT_EQ(source.GetMaterialAssetRefs().size(), 1u); EXPECT_TRUE(source.GetMaterialAssetRefs()[0].IsValid()); std::stringstream stream; source.Serialize(stream); const std::string serialized = stream.str(); EXPECT_NE(serialized.find("materialPaths=;"), std::string::npos); EXPECT_NE(serialized.find("materialRefs="), std::string::npos); EXPECT_EQ(serialized.find("materialRefs=;"), std::string::npos); std::stringstream deserializeStream(serialized); MeshRendererComponent target; target.Deserialize(deserializeStream); ASSERT_EQ(target.GetMaterialCount(), 1u); EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material"); ASSERT_NE(target.GetMaterial(0), nullptr); EXPECT_TRUE(target.GetMaterialAssetRefs()[0].IsValid()); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } TEST(MeshRendererComponent_Test, DeferredSceneDeserializeLoadsProjectMaterialAsync) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_renderer_async_asset_ref_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path materialPath = assetsDir / "runtime.material"; fs::remove_all(projectRoot); fs::create_directories(assetsDir); { std::ofstream materialFile(materialPath); ASSERT_TRUE(materialFile.is_open()); materialFile << "{\n"; materialFile << " \"renderQueue\": \"geometry\",\n"; materialFile << " \"renderState\": {\n"; materialFile << " \"cull\": \"back\"\n"; materialFile << " }\n"; materialFile << "}"; } manager.SetResourceRoot(projectRoot.string().c_str()); MeshRendererComponent source; source.SetMaterialPath(0, "Assets/runtime.material"); std::stringstream serializedStream; source.Serialize(serializedStream); MeshRendererComponent target; const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount(); { ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled()); std::stringstream deserializeStream(serializedStream.str()); target.Deserialize(deserializeStream); EXPECT_EQ(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); } ASSERT_EQ(target.GetMaterialCount(), 1u); EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material"); EXPECT_EQ(target.GetMaterial(0), nullptr); EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager)); ASSERT_NE(target.GetMaterial(0), nullptr); EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material"); EXPECT_TRUE(target.GetMaterialAssetRefs()[0].IsValid()); manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } } // namespace