Add deferred async scene asset loading
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
158
tests/core/Asset/test_resource_manager.cpp
Normal file
158
tests/core/Asset/test_resource_manager.cpp
Normal 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
|
||||
Reference in New Issue
Block a user