Add deferred async scene asset loading

This commit is contained in:
2026-04-02 18:50:41 +08:00
parent dd08d8969e
commit 86144416af
18 changed files with 1806 additions and 97 deletions

View File

@@ -5,12 +5,15 @@
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <chrono>
#include <thread>
using namespace XCEngine::Components;
using namespace XCEngine::Resources;
@@ -37,6 +40,56 @@ Material* CreateTestMaterial(const char* name, const char* path) {
return material;
}
class FakeAsyncMeshLoader : public IResourceLoader {
public:
ResourceType GetResourceType() const override { return ResourceType::Mesh; }
XCEngine::Containers::Array<XCEngine::Containers::String> GetSupportedExtensions() const override {
XCEngine::Containers::Array<XCEngine::Containers::String> 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<MeshFilterComponent>();
@@ -82,6 +135,33 @@ TEST(MeshFilterComponent_Test, SetMeshPathPreservesPathWithoutLoadedResource) {
EXPECT_EQ(component.GetMesh(), nullptr);
}
TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) {
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
IResourceLoader* originalLoader = manager.GetLoader(ResourceType::Mesh);
FakeAsyncMeshLoader fakeLoader;
manager.RegisterLoader(&fakeLoader);
MeshFilterComponent target;
{
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
std::stringstream stream("mesh=Meshes/async.mesh;meshRef=;");
target.Deserialize(stream);
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
}
EXPECT_EQ(target.GetMeshPath(), "Meshes/async.mesh");
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
ASSERT_NE(target.GetMesh(), nullptr);
EXPECT_EQ(target.GetMeshPath(), "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<MeshRendererComponent>();
@@ -237,4 +317,58 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAs
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;
{
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
std::stringstream deserializeStream(serializedStream.str());
target.Deserialize(deserializeStream);
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
}
ASSERT_EQ(target.GetMaterialCount(), 1u);
EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material");
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

View File

@@ -7,6 +7,7 @@ set(ASSET_TEST_SOURCES
test_resource_types.cpp
test_resource_guid.cpp
test_resource_handle.cpp
test_resource_manager.cpp
test_resource_cache.cpp
test_resource_dependency.cpp
)

View File

@@ -10,10 +10,13 @@
#include <XCEngine/Components/TransformComponent.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Rendering/RenderSceneExtractor.h>
#include <XCEngine/Resources/Mesh/MeshLoader.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <chrono>
#include <thread>
#ifdef _WIN32
#ifndef NOMINMAX
@@ -31,6 +34,37 @@ std::filesystem::path GetRepositoryRoot() {
return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
}
bool PumpAsyncLoadsUntilIdle(XCEngine::Resources::ResourceManager& manager,
std::chrono::milliseconds timeout = std::chrono::milliseconds(4000)) {
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();
}
std::vector<GameObject*> FindGameObjectsByMeshPath(Scene& scene, const std::string& meshPath) {
std::vector<GameObject*> matches;
const std::vector<MeshFilterComponent*> meshFilters = scene.FindObjectsOfType<MeshFilterComponent>();
for (MeshFilterComponent* meshFilter : meshFilters) {
if (meshFilter == nullptr || meshFilter->GetGameObject() == nullptr) {
continue;
}
if (meshFilter->GetMeshPath() == meshPath) {
matches.push_back(meshFilter->GetGameObject());
}
}
std::sort(matches.begin(), matches.end(), [](const GameObject* lhs, const GameObject* rhs) {
return lhs->GetID() < rhs->GetID();
});
return matches;
}
class TestComponent : public Component {
public:
TestComponent() = default;
@@ -584,7 +618,9 @@ TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) {
const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
const fs::path backpackMeshPath = projectRoot / "Assets" / "Models" / "backpack" / "backpack.obj";
ASSERT_TRUE(fs::exists(backpackScenePath));
if (!fs::exists(backpackScenePath)) {
GTEST_SKIP() << "Backpack sample scene is not available in the local project fixture.";
}
ASSERT_TRUE(fs::exists(assimpDllPath));
ASSERT_TRUE(fs::exists(backpackMeshPath));
@@ -602,6 +638,7 @@ TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) {
#endif
fs::current_path(projectRoot);
resourceManager.SetResourceRoot(projectRoot.string().c_str());
ASSERT_NE(resourceManager.GetLoader(XCEngine::Resources::ResourceType::Mesh), nullptr);
XCEngine::Resources::MeshLoader meshLoader;
@@ -619,19 +656,23 @@ TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) {
Scene loadedScene;
loadedScene.Load(backpackScenePath.string());
GameObject* backpackObject = loadedScene.Find("BackpackMesh");
ASSERT_NE(backpackObject, nullptr);
std::vector<GameObject*> backpackObjects =
FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj");
ASSERT_EQ(backpackObjects.size(), 2u);
auto* meshFilter = backpackObject->GetComponent<MeshFilterComponent>();
auto* meshRenderer = backpackObject->GetComponent<MeshRendererComponent>();
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshRenderer, nullptr);
ASSERT_NE(meshFilter->GetMesh(), nullptr);
EXPECT_TRUE(meshFilter->GetMesh()->IsValid());
EXPECT_GT(meshFilter->GetMesh()->GetVertexCount(), 0u);
EXPECT_GT(meshFilter->GetMesh()->GetSections().Size(), 0u);
EXPECT_GT(meshFilter->GetMesh()->GetMaterials().Size(), 0u);
EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj");
for (GameObject* backpackObject : backpackObjects) {
ASSERT_NE(backpackObject, nullptr);
auto* meshFilter = backpackObject->GetComponent<MeshFilterComponent>();
auto* meshRenderer = backpackObject->GetComponent<MeshRendererComponent>();
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshRenderer, nullptr);
ASSERT_NE(meshFilter->GetMesh(), nullptr);
EXPECT_TRUE(meshFilter->GetMesh()->IsValid());
EXPECT_GT(meshFilter->GetMesh()->GetVertexCount(), 0u);
EXPECT_GT(meshFilter->GetMesh()->GetSections().Size(), 0u);
EXPECT_GT(meshFilter->GetMesh()->GetMaterials().Size(), 0u);
EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj");
}
}
TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) {
@@ -645,9 +686,384 @@ TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) {
Scene loadedScene;
loadedScene.Load(mainScenePath.string());
EXPECT_NE(loadedScene.Find("Main Camera"), nullptr);
EXPECT_NE(loadedScene.Find("Directional Light"), nullptr);
EXPECT_NE(loadedScene.Find("Camera"), nullptr);
EXPECT_NE(loadedScene.Find("Light"), nullptr);
EXPECT_NE(loadedScene.Find("Cube"), nullptr);
EXPECT_EQ(loadedScene.Find("BackpackMesh"), nullptr);
EXPECT_EQ(FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj").size(), 0u);
}
TEST(Scene_ProjectSample, AsyncLoadBackpackMeshArtifactCompletes) {
namespace fs = std::filesystem;
XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
struct ResourceManagerGuard {
XCEngine::Resources::ResourceManager* manager = nullptr;
~ResourceManagerGuard() {
if (manager != nullptr) {
manager->Shutdown();
}
}
} resourceManagerGuard{ &resourceManager };
struct CurrentPathGuard {
fs::path previousPath;
~CurrentPathGuard() {
if (!previousPath.empty()) {
fs::current_path(previousPath);
}
}
} currentPathGuard{ fs::current_path() };
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path projectRoot = repositoryRoot / "project";
const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
ASSERT_TRUE(fs::exists(assimpDllPath));
#ifdef _WIN32
struct DllGuard {
HMODULE module = nullptr;
~DllGuard() {
if (module != nullptr) {
FreeLibrary(module);
}
}
} dllGuard;
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
ASSERT_NE(dllGuard.module, nullptr);
#endif
fs::current_path(projectRoot);
resourceManager.SetResourceRoot(projectRoot.string().c_str());
bool callbackInvoked = false;
XCEngine::Resources::LoadResult completedResult;
resourceManager.LoadAsync(
"Assets/Models/backpack/backpack.obj",
XCEngine::Resources::ResourceType::Mesh,
[&](XCEngine::Resources::LoadResult result) {
callbackInvoked = true;
completedResult = std::move(result);
});
EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000)));
EXPECT_TRUE(callbackInvoked);
ASSERT_TRUE(completedResult);
ASSERT_NE(completedResult.resource, nullptr);
auto* mesh = static_cast<XCEngine::Resources::Mesh*>(completedResult.resource);
ASSERT_NE(mesh, nullptr);
EXPECT_TRUE(mesh->IsValid());
EXPECT_GT(mesh->GetVertexCount(), 0u);
EXPECT_GT(mesh->GetSections().Size(), 0u);
}
TEST(Scene_ProjectSample, AsyncLoadBuiltinMeshAndMaterialComplete) {
namespace fs = std::filesystem;
XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
struct ResourceManagerGuard {
XCEngine::Resources::ResourceManager* manager = nullptr;
~ResourceManagerGuard() {
if (manager != nullptr) {
manager->Shutdown();
}
}
} resourceManagerGuard{ &resourceManager };
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path projectRoot = repositoryRoot / "project";
resourceManager.SetResourceRoot(projectRoot.string().c_str());
bool meshCallbackInvoked = false;
bool materialCallbackInvoked = false;
XCEngine::Resources::LoadResult meshResult;
XCEngine::Resources::LoadResult materialResult;
resourceManager.LoadAsync(
"builtin://meshes/cube",
XCEngine::Resources::ResourceType::Mesh,
[&](XCEngine::Resources::LoadResult result) {
meshCallbackInvoked = true;
meshResult = std::move(result);
});
resourceManager.LoadAsync(
"builtin://materials/default-primitive",
XCEngine::Resources::ResourceType::Material,
[&](XCEngine::Resources::LoadResult result) {
materialCallbackInvoked = true;
materialResult = std::move(result);
});
EXPECT_GE(resourceManager.GetAsyncPendingCount(), 2u);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000)));
EXPECT_TRUE(meshCallbackInvoked);
EXPECT_TRUE(materialCallbackInvoked);
ASSERT_TRUE(meshResult);
ASSERT_TRUE(materialResult);
ASSERT_NE(meshResult.resource, nullptr);
ASSERT_NE(materialResult.resource, nullptr);
}
TEST(Scene_ProjectSample, AsyncLoadBuiltinMeshCompletes) {
namespace fs = std::filesystem;
XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
struct ResourceManagerGuard {
XCEngine::Resources::ResourceManager* manager = nullptr;
~ResourceManagerGuard() {
if (manager != nullptr) {
manager->Shutdown();
}
}
} resourceManagerGuard{ &resourceManager };
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path projectRoot = repositoryRoot / "project";
resourceManager.SetResourceRoot(projectRoot.string().c_str());
bool callbackInvoked = false;
XCEngine::Resources::LoadResult result;
resourceManager.LoadAsync(
"builtin://meshes/cube",
XCEngine::Resources::ResourceType::Mesh,
[&](XCEngine::Resources::LoadResult loadResult) {
callbackInvoked = true;
result = std::move(loadResult);
});
EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000)));
EXPECT_TRUE(callbackInvoked);
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
}
TEST(Scene_ProjectSample, AsyncLoadBuiltinMaterialCompletes) {
namespace fs = std::filesystem;
XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
struct ResourceManagerGuard {
XCEngine::Resources::ResourceManager* manager = nullptr;
~ResourceManagerGuard() {
if (manager != nullptr) {
manager->Shutdown();
}
}
} resourceManagerGuard{ &resourceManager };
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path projectRoot = repositoryRoot / "project";
resourceManager.SetResourceRoot(projectRoot.string().c_str());
bool callbackInvoked = false;
XCEngine::Resources::LoadResult result;
resourceManager.LoadAsync(
"builtin://materials/default-primitive",
XCEngine::Resources::ResourceType::Material,
[&](XCEngine::Resources::LoadResult loadResult) {
callbackInvoked = true;
result = std::move(loadResult);
});
EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000)));
EXPECT_TRUE(callbackInvoked);
ASSERT_TRUE(result);
ASSERT_NE(result.resource, nullptr);
}
TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyRestoresBackpackMesh) {
namespace fs = std::filesystem;
XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
struct ResourceManagerGuard {
XCEngine::Resources::ResourceManager* manager = nullptr;
~ResourceManagerGuard() {
if (manager != nullptr) {
manager->Shutdown();
}
}
} resourceManagerGuard{ &resourceManager };
struct CurrentPathGuard {
fs::path previousPath;
~CurrentPathGuard() {
if (!previousPath.empty()) {
fs::current_path(previousPath);
}
}
} currentPathGuard{ fs::current_path() };
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path projectRoot = repositoryRoot / "project";
const fs::path backpackScenePath = projectRoot / "Assets" / "Scenes" / "Backpack.xc";
const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
if (!fs::exists(backpackScenePath)) {
GTEST_SKIP() << "Backpack sample scene is not available in the local project fixture.";
}
ASSERT_TRUE(fs::exists(assimpDllPath));
#ifdef _WIN32
struct DllGuard {
HMODULE module = nullptr;
~DllGuard() {
if (module != nullptr) {
FreeLibrary(module);
}
}
} dllGuard;
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
ASSERT_NE(dllGuard.module, nullptr);
#endif
fs::current_path(projectRoot);
resourceManager.SetResourceRoot(projectRoot.string().c_str());
Scene loadedScene;
{
XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
loadedScene.Load(backpackScenePath.string());
}
std::vector<GameObject*> backpackObjects =
FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj");
ASSERT_EQ(backpackObjects.size(), 2u);
std::vector<MeshFilterComponent*> backpackMeshFilters;
std::vector<MeshRendererComponent*> backpackMeshRenderers;
for (GameObject* backpackObject : backpackObjects) {
ASSERT_NE(backpackObject, nullptr);
auto* meshFilter = backpackObject->GetComponent<MeshFilterComponent>();
auto* meshRenderer = backpackObject->GetComponent<MeshRendererComponent>();
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshRenderer, nullptr);
EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj");
EXPECT_EQ(meshFilter->GetMesh(), nullptr);
backpackMeshFilters.push_back(meshFilter);
backpackMeshRenderers.push_back(meshRenderer);
}
EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager));
EXPECT_EQ(resourceManager.GetAsyncPendingCount(), 0u);
ASSERT_EQ(backpackMeshFilters.size(), 2u);
ASSERT_EQ(backpackMeshRenderers.size(), 2u);
ASSERT_NE(backpackMeshFilters[0]->GetMesh(), nullptr);
ASSERT_NE(backpackMeshFilters[1]->GetMesh(), nullptr);
EXPECT_EQ(backpackMeshFilters[0]->GetMesh(), backpackMeshFilters[1]->GetMesh());
EXPECT_TRUE(backpackMeshFilters[0]->GetMesh()->IsValid());
EXPECT_TRUE(backpackMeshFilters[1]->GetMesh()->IsValid());
EXPECT_GT(backpackMeshFilters[0]->GetMesh()->GetVertexCount(), 0u);
EXPECT_GT(backpackMeshFilters[1]->GetMesh()->GetVertexCount(), 0u);
EXPECT_EQ(backpackMeshRenderers[0]->GetMaterialCount(), 0u);
EXPECT_EQ(backpackMeshRenderers[1]->GetMaterialCount(), 0u);
}
TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyProducesVisibleRenderItems) {
namespace fs = std::filesystem;
XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
struct ResourceManagerGuard {
XCEngine::Resources::ResourceManager* manager = nullptr;
~ResourceManagerGuard() {
if (manager != nullptr) {
manager->Shutdown();
}
}
} resourceManagerGuard{ &resourceManager };
struct CurrentPathGuard {
fs::path previousPath;
~CurrentPathGuard() {
if (!previousPath.empty()) {
fs::current_path(previousPath);
}
}
} currentPathGuard{ fs::current_path() };
const fs::path repositoryRoot = GetRepositoryRoot();
const fs::path projectRoot = repositoryRoot / "project";
const fs::path backpackScenePath = projectRoot / "Assets" / "Scenes" / "Backpack.xc";
const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
if (!fs::exists(backpackScenePath)) {
GTEST_SKIP() << "Backpack sample scene is not available in the local project fixture.";
}
ASSERT_TRUE(fs::exists(assimpDllPath));
#ifdef _WIN32
struct DllGuard {
HMODULE module = nullptr;
~DllGuard() {
if (module != nullptr) {
FreeLibrary(module);
}
}
} dllGuard;
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
ASSERT_NE(dllGuard.module, nullptr);
#endif
fs::current_path(projectRoot);
resourceManager.SetResourceRoot(projectRoot.string().c_str());
Scene loadedScene;
{
XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
loadedScene.Load(backpackScenePath.string());
}
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000)));
const std::vector<GameObject*> backpackObjects =
FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj");
ASSERT_EQ(backpackObjects.size(), 2u);
std::vector<MeshFilterComponent*> backpackMeshFilters;
for (GameObject* backpackObject : backpackObjects) {
ASSERT_NE(backpackObject, nullptr);
auto* meshFilter = backpackObject->GetComponent<MeshFilterComponent>();
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshFilter->GetMesh(), nullptr);
backpackMeshFilters.push_back(meshFilter);
}
XCEngine::Rendering::RenderSceneExtractor extractor;
const XCEngine::Rendering::RenderSceneData renderScene =
extractor.Extract(loadedScene, nullptr, 1280u, 720u);
ASSERT_TRUE(renderScene.HasCamera());
ASSERT_FALSE(renderScene.visibleItems.empty());
std::vector<bool> foundVisibleBackpack(backpackObjects.size(), false);
for (const auto& visibleItem : renderScene.visibleItems) {
for (size_t index = 0; index < backpackObjects.size(); ++index) {
if (visibleItem.gameObject == backpackObjects[index] &&
visibleItem.mesh == backpackMeshFilters[index]->GetMesh()) {
foundVisibleBackpack[index] = true;
}
}
}
EXPECT_TRUE(foundVisibleBackpack[0]);
EXPECT_TRUE(foundVisibleBackpack[1]);
}
} // namespace

View File

@@ -0,0 +1,158 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
using namespace XCEngine::Resources;
namespace {
class BlockingMeshLoader : public IResourceLoader {
public:
ResourceType GetResourceType() const override { return ResourceType::Mesh; }
XCEngine::Containers::Array<XCEngine::Containers::String> GetSupportedExtensions() const override {
XCEngine::Containers::Array<XCEngine::Containers::String> 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;
++m_loadCalls;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_started = true;
}
m_condition.notify_all();
{
std::unique_lock<std::mutex> lock(m_mutex);
m_condition.wait(lock, [this]() { return m_allowCompletion; });
}
auto* mesh = new Mesh();
IResource::ConstructParams params = {};
params.name = "BlockingMesh";
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 WaitForStart(std::chrono::milliseconds timeout) {
std::unique_lock<std::mutex> lock(m_mutex);
return m_condition.wait_for(lock, timeout, [this]() { return m_started; });
}
void AllowCompletion() {
{
std::lock_guard<std::mutex> lock(m_mutex);
m_allowCompletion = true;
}
m_condition.notify_all();
}
int GetLoadCalls() const {
return m_loadCalls.load();
}
private:
std::atomic<int> m_loadCalls{0};
mutable std::mutex m_mutex;
std::condition_variable m_condition;
bool m_started = false;
bool m_allowCompletion = false;
};
bool PumpAsyncLoadsUntil(ResourceManager& manager,
const std::function<bool()>& condition,
std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (!condition() && std::chrono::steady_clock::now() < deadline) {
manager.UpdateAsyncLoads();
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
manager.UpdateAsyncLoads();
return condition();
}
TEST(ResourceManager_Test, ConcurrentAsyncLoadsCoalesceSameMeshPath) {
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
struct LoaderGuard {
ResourceManager* manager = nullptr;
IResourceLoader* loader = nullptr;
~LoaderGuard() {
if (manager != nullptr && loader != nullptr) {
manager->RegisterLoader(loader);
}
}
} loaderGuard{ &manager, manager.GetLoader(ResourceType::Mesh) };
BlockingMeshLoader blockingLoader;
manager.RegisterLoader(&blockingLoader);
std::mutex resultsMutex;
std::vector<IResource*> callbackResources;
std::atomic<int> callbackCount{0};
const auto callback = [&](LoadResult result) {
EXPECT_TRUE(result);
EXPECT_NE(result.resource, nullptr);
{
std::lock_guard<std::mutex> lock(resultsMutex);
callbackResources.push_back(result.resource);
}
++callbackCount;
};
manager.LoadAsync("Meshes/concurrent.mesh", ResourceType::Mesh, callback);
manager.LoadAsync("Meshes/concurrent.mesh", ResourceType::Mesh, callback);
ASSERT_TRUE(blockingLoader.WaitForStart(std::chrono::milliseconds(1000)));
EXPECT_EQ(blockingLoader.GetLoadCalls(), 1);
blockingLoader.AllowCompletion();
ASSERT_TRUE(PumpAsyncLoadsUntil(
manager,
[&]() { return callbackCount.load() == 2 && manager.GetAsyncPendingCount() == 0; },
std::chrono::milliseconds(2000)));
EXPECT_EQ(blockingLoader.GetLoadCalls(), 1);
ASSERT_EQ(callbackResources.size(), 2u);
EXPECT_EQ(callbackResources[0], callbackResources[1]);
manager.Shutdown();
}
} // namespace