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

@@ -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