Add deferred async scene asset loading
This commit is contained in:
@@ -9,7 +9,9 @@
|
||||
#include "UI/BuiltInIcons.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Platform/WindowsProcessDiagnostics.h"
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <chrono>
|
||||
#include <windows.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -141,12 +143,18 @@ bool Application::Initialize(HWND hwnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
|
||||
resourceManager.Initialize();
|
||||
resourceManager.SetResourceRoot(projectRoot.c_str());
|
||||
m_resourceManagerInitialized = true;
|
||||
|
||||
logger.Info(Debug::LogCategory::General, "Initializing editor context...");
|
||||
InitializeEditorContext(projectRoot);
|
||||
logger.Info(Debug::LogCategory::General, "Initializing ImGui backend...");
|
||||
InitializeImGui(hwnd);
|
||||
logger.Info(Debug::LogCategory::General, "Attaching editor layer...");
|
||||
AttachEditorLayer();
|
||||
m_hasLastFrameTime = false;
|
||||
logger.Info(Debug::LogCategory::General, "Editor initialization completed.");
|
||||
m_renderReady = true;
|
||||
return true;
|
||||
@@ -154,6 +162,7 @@ bool Application::Initialize(HWND hwnd) {
|
||||
|
||||
void Application::Shutdown() {
|
||||
m_renderReady = false;
|
||||
m_hasLastFrameTime = false;
|
||||
DetachEditorLayer();
|
||||
if (m_editorContext) {
|
||||
static_cast<EditorContext*>(m_editorContext.get())->SetViewportHostService(nullptr);
|
||||
@@ -163,6 +172,10 @@ void Application::Shutdown() {
|
||||
m_imguiBackend.Shutdown();
|
||||
m_imguiSession.Shutdown();
|
||||
ShutdownEditorContext();
|
||||
if (m_resourceManagerInitialized) {
|
||||
::XCEngine::Resources::ResourceManager::Get().Shutdown();
|
||||
m_resourceManagerInitialized = false;
|
||||
}
|
||||
m_windowRenderer.Shutdown();
|
||||
}
|
||||
|
||||
@@ -170,6 +183,16 @@ void Application::Render() {
|
||||
if (!m_renderReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
float deltaTime = 0.0f;
|
||||
if (m_hasLastFrameTime) {
|
||||
deltaTime = std::chrono::duration<float>(now - m_lastFrameTime).count();
|
||||
}
|
||||
m_lastFrameTime = now;
|
||||
m_hasLastFrameTime = true;
|
||||
|
||||
m_layerStack.onUpdate(deltaTime);
|
||||
RenderEditorFrame();
|
||||
}
|
||||
|
||||
@@ -206,6 +229,8 @@ bool Application::SwitchProject(const std::string& projectPath) {
|
||||
const std::string infoMessage = "Switched editor project root: " + projectPath;
|
||||
logger.Info(Debug::LogCategory::General, infoMessage.c_str());
|
||||
|
||||
::XCEngine::Resources::ResourceManager::Get().SetResourceRoot(projectPath.c_str());
|
||||
|
||||
m_lastWindowTitle.clear();
|
||||
UpdateWindowTitle();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "Viewport/ViewportHostService.h"
|
||||
|
||||
#include <XCEngine/Rendering/RenderContext.h>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
@@ -65,7 +66,10 @@ private:
|
||||
ViewportHostService m_viewportHostService;
|
||||
uint64_t m_exitRequestedHandlerId = 0;
|
||||
std::wstring m_lastWindowTitle;
|
||||
std::chrono::steady_clock::time_point m_lastFrameTime{};
|
||||
bool m_hasLastFrameTime = false;
|
||||
bool m_renderReady = false;
|
||||
bool m_resourceManagerInitialized = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "panels/ProjectPanel.h"
|
||||
#include "panels/SceneViewPanel.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -53,6 +55,7 @@ public:
|
||||
}
|
||||
|
||||
void Update(float dt) {
|
||||
::XCEngine::Resources::ResourceManager::Get().UpdateAsyncLoads();
|
||||
m_panels.UpdateAll(dt);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Core/EventBus.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Utils/ProjectFileUtils.h"
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Components/ComponentFactoryRegistry.h>
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
@@ -192,7 +193,10 @@ bool SceneManager::RestoreSceneSnapshot(const SceneSnapshot& snapshot) {
|
||||
}
|
||||
|
||||
auto scene = std::make_unique<::XCEngine::Components::Scene>();
|
||||
scene->DeserializeFromString(snapshot.sceneData);
|
||||
{
|
||||
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
|
||||
scene->DeserializeFromString(snapshot.sceneData);
|
||||
}
|
||||
|
||||
m_scene = std::move(scene);
|
||||
m_clipboard.reset();
|
||||
@@ -245,7 +249,10 @@ bool SceneManager::LoadScene(const std::string& filePath) {
|
||||
}
|
||||
|
||||
auto scene = std::make_unique<::XCEngine::Components::Scene>();
|
||||
scene->Load(filePath);
|
||||
{
|
||||
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
|
||||
scene->Load(filePath);
|
||||
}
|
||||
|
||||
m_scene = std::move(scene);
|
||||
m_clipboard.reset();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -14,8 +15,8 @@ class MeshFilterComponent : public Component {
|
||||
public:
|
||||
std::string GetName() const override { return "MeshFilter"; }
|
||||
|
||||
Resources::Mesh* GetMesh() const { return m_mesh.Get(); }
|
||||
const Resources::ResourceHandle<Resources::Mesh>& GetMeshHandle() const { return m_mesh; }
|
||||
Resources::Mesh* GetMesh() const;
|
||||
const Resources::ResourceHandle<Resources::Mesh>& GetMeshHandle() const;
|
||||
const std::string& GetMeshPath() const { return m_meshPath; }
|
||||
const Resources::AssetRef& GetMeshAssetRef() const { return m_meshRef; }
|
||||
|
||||
@@ -28,9 +29,15 @@ public:
|
||||
void Deserialize(std::istream& is) override;
|
||||
|
||||
private:
|
||||
struct PendingMeshLoadState;
|
||||
|
||||
void BeginAsyncMeshLoad(const std::string& meshPath);
|
||||
void ResolvePendingMesh();
|
||||
|
||||
Resources::ResourceHandle<Resources::Mesh> m_mesh;
|
||||
std::string m_meshPath;
|
||||
Resources::AssetRef m_meshRef;
|
||||
std::shared_ptr<PendingMeshLoadState> m_pendingMeshLoad;
|
||||
};
|
||||
|
||||
} // namespace Components
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -42,12 +43,17 @@ public:
|
||||
void Deserialize(std::istream& is) override;
|
||||
|
||||
private:
|
||||
struct PendingMaterialLoadState;
|
||||
|
||||
void BeginAsyncMaterialLoad(size_t index, const std::string& materialPath);
|
||||
void ResolvePendingMaterials();
|
||||
void EnsureMaterialSlot(size_t index);
|
||||
static std::string MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material);
|
||||
|
||||
std::vector<Resources::ResourceHandle<Resources::Material>> m_materials;
|
||||
std::vector<std::string> m_materialPaths;
|
||||
std::vector<Resources::AssetRef> m_materialRefs;
|
||||
std::vector<std::shared_ptr<PendingMaterialLoadState>> m_pendingMaterialLoads;
|
||||
bool m_castShadows = true;
|
||||
bool m_receiveShadows = true;
|
||||
uint32_t m_renderLayer = 0;
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include <XCEngine/Core/Asset/ImportSettings.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Threading/Mutex.h>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
@@ -50,6 +53,14 @@ private:
|
||||
static Core::uint64 GenerateRequestId();
|
||||
};
|
||||
|
||||
struct CompletedLoadRequest {
|
||||
LoadRequest request;
|
||||
LoadResult result;
|
||||
|
||||
CompletedLoadRequest(LoadRequest inRequest, LoadResult inResult)
|
||||
: request(std::move(inRequest)), result(std::move(inResult)) {}
|
||||
};
|
||||
|
||||
class AsyncLoader {
|
||||
public:
|
||||
static AsyncLoader& Get();
|
||||
@@ -76,18 +87,21 @@ public:
|
||||
private:
|
||||
void SubmitInternal(LoadRequest request);
|
||||
IResourceLoader* FindLoader(ResourceType type) const;
|
||||
|
||||
void WorkerThread();
|
||||
void QueueCompleted(LoadRequest request, LoadResult result);
|
||||
|
||||
Threading::Mutex m_queueMutex;
|
||||
Containers::Array<LoadRequest> m_pendingQueue;
|
||||
std::mutex m_queueMutex;
|
||||
std::condition_variable m_pendingCondition;
|
||||
std::deque<LoadRequest> m_pendingQueue;
|
||||
|
||||
Threading::Mutex m_completedMutex;
|
||||
Containers::Array<LoadRequest> m_completedQueue;
|
||||
std::mutex m_completedMutex;
|
||||
std::deque<CompletedLoadRequest> m_completedQueue;
|
||||
std::vector<std::thread> m_workerThreads;
|
||||
|
||||
std::atomic<bool> m_running{false};
|
||||
std::atomic<Core::uint32> m_pendingCount{0};
|
||||
std::atomic<Core::uint32> m_completedCount{0};
|
||||
Core::uint32 m_totalRequested = 0;
|
||||
std::atomic<Core::uint64> m_totalRequested{0};
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
#include <XCEngine/Core/Containers/HashMap.h>
|
||||
#include <XCEngine/Threading/Mutex.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <type_traits>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -18,6 +23,17 @@ namespace Resources {
|
||||
|
||||
class ResourceManager {
|
||||
public:
|
||||
class ScopedDeferredSceneLoad {
|
||||
public:
|
||||
explicit ScopedDeferredSceneLoad(ResourceManager& manager = ResourceManager::Get());
|
||||
ScopedDeferredSceneLoad(const ScopedDeferredSceneLoad&) = delete;
|
||||
ScopedDeferredSceneLoad& operator=(const ScopedDeferredSceneLoad&) = delete;
|
||||
~ScopedDeferredSceneLoad();
|
||||
|
||||
private:
|
||||
ResourceManager* m_manager = nullptr;
|
||||
};
|
||||
|
||||
static ResourceManager& Get();
|
||||
|
||||
void Initialize();
|
||||
@@ -43,7 +59,7 @@ public:
|
||||
static_assert(std::is_base_of_v<IResource, T>, "T must derive from IResource");
|
||||
|
||||
Containers::String path;
|
||||
if (!assetRef.IsValid() || !m_assetDatabase.TryGetPrimaryAssetPath(assetRef.assetGuid, path)) {
|
||||
if (!TryResolveAssetPath(assetRef, path)) {
|
||||
return ResourceHandle<T>();
|
||||
}
|
||||
|
||||
@@ -54,6 +70,9 @@ public:
|
||||
std::function<void(LoadResult)> callback);
|
||||
void LoadAsync(const Containers::String& path, ResourceType type, ImportSettings* settings,
|
||||
std::function<void(LoadResult)> callback);
|
||||
void UpdateAsyncLoads();
|
||||
bool IsAsyncLoading() const;
|
||||
Core::uint32 GetAsyncPendingCount() const;
|
||||
|
||||
void Unload(const Containers::String& path);
|
||||
void Unload(ResourceGUID guid);
|
||||
@@ -98,8 +117,35 @@ public:
|
||||
void UnloadGroup(const Containers::Array<ResourceGUID>& guids);
|
||||
void RefreshAssetDatabase();
|
||||
bool TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) const;
|
||||
void BeginDeferredSceneLoad();
|
||||
void EndDeferredSceneLoad();
|
||||
bool IsDeferredSceneLoadEnabled() const;
|
||||
|
||||
private:
|
||||
struct InFlightLoadKey {
|
||||
ResourceGUID guid;
|
||||
ResourceType type = ResourceType::Unknown;
|
||||
|
||||
bool operator==(const InFlightLoadKey& other) const {
|
||||
return guid == other.guid && type == other.type;
|
||||
}
|
||||
};
|
||||
|
||||
struct InFlightLoadKeyHasher {
|
||||
size_t operator()(const InFlightLoadKey& key) const noexcept {
|
||||
return std::hash<ResourceGUID>{}(key.guid) ^
|
||||
(static_cast<size_t>(key.type) << 1);
|
||||
}
|
||||
};
|
||||
|
||||
struct InFlightLoadState {
|
||||
bool completed = false;
|
||||
bool success = false;
|
||||
Containers::String errorMessage;
|
||||
std::condition_variable condition;
|
||||
};
|
||||
|
||||
ResourceManager() = default;
|
||||
~ResourceManager() = default;
|
||||
|
||||
@@ -122,8 +168,13 @@ private:
|
||||
ResourceCache m_cache;
|
||||
Core::UniqueRef<AsyncLoader> m_asyncLoader;
|
||||
Threading::Mutex m_mutex;
|
||||
mutable std::recursive_mutex m_ioMutex;
|
||||
std::mutex m_inFlightLoadsMutex;
|
||||
std::unordered_map<InFlightLoadKey, std::shared_ptr<InFlightLoadState>, InFlightLoadKeyHasher> m_inFlightLoads;
|
||||
std::atomic<Core::uint32> m_deferredSceneLoadDepth{0};
|
||||
|
||||
friend class ResourceHandleBase;
|
||||
friend class AsyncLoader;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "Components/MeshFilterComponent.h"
|
||||
|
||||
#include "Components/GameObject.h"
|
||||
#include "Core/Asset/ResourceManager.h"
|
||||
#include "Debug/Logger.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
@@ -13,6 +15,11 @@ std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
bool ShouldTraceMeshPath(const std::string& path) {
|
||||
return path.rfind("builtin://", 0) == 0 ||
|
||||
path.find("backpack") != std::string::npos;
|
||||
}
|
||||
|
||||
std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
|
||||
if (!assetRef.IsValid()) {
|
||||
return std::string();
|
||||
@@ -37,9 +44,40 @@ bool TryDecodeAssetRef(const std::string& value, Resources::AssetRef& outRef) {
|
||||
return outRef.IsValid();
|
||||
}
|
||||
|
||||
std::string DescribeOwner(const MeshFilterComponent& component) {
|
||||
const GameObject* owner = component.GetGameObject();
|
||||
if (owner == nullptr) {
|
||||
return "<no-owner>";
|
||||
}
|
||||
|
||||
return std::string("id=") + std::to_string(owner->GetID()) + ",name=" + owner->GetName();
|
||||
}
|
||||
|
||||
void TraceMeshFilter(const MeshFilterComponent& component, const std::string& message) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String(("[MeshFilter] owner={" + DescribeOwner(component) + "} " + message).c_str()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct MeshFilterComponent::PendingMeshLoadState {
|
||||
Resources::LoadResult result;
|
||||
bool completed = false;
|
||||
};
|
||||
|
||||
Resources::Mesh* MeshFilterComponent::GetMesh() const {
|
||||
const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh();
|
||||
return m_mesh.Get();
|
||||
}
|
||||
|
||||
const Resources::ResourceHandle<Resources::Mesh>& MeshFilterComponent::GetMeshHandle() const {
|
||||
const_cast<MeshFilterComponent*>(this)->ResolvePendingMesh();
|
||||
return m_mesh;
|
||||
}
|
||||
|
||||
void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_meshPath = meshPath;
|
||||
if (m_meshPath.empty()) {
|
||||
m_mesh.Reset();
|
||||
@@ -51,9 +89,18 @@ void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
||||
m_meshRef.Reset();
|
||||
}
|
||||
|
||||
if (ShouldTraceMeshPath(m_meshPath)) {
|
||||
TraceMeshFilter(
|
||||
*this,
|
||||
std::string("SetMeshPath path=") + m_meshPath +
|
||||
" ref=" + EncodeAssetRef(m_meshRef) +
|
||||
" loaded=" + (m_mesh.Get() != nullptr ? "1" : "0"));
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_mesh = mesh;
|
||||
m_meshPath = mesh.Get() != nullptr ? ToStdString(mesh->GetPath()) : std::string();
|
||||
if (m_meshPath.empty()) {
|
||||
@@ -68,6 +115,7 @@ void MeshFilterComponent::SetMesh(Resources::Mesh* mesh) {
|
||||
}
|
||||
|
||||
void MeshFilterComponent::ClearMesh() {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_mesh.Reset();
|
||||
m_meshPath.clear();
|
||||
m_meshRef.Reset();
|
||||
@@ -79,6 +127,7 @@ void MeshFilterComponent::Serialize(std::ostream& os) const {
|
||||
}
|
||||
|
||||
void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_mesh.Reset();
|
||||
m_meshPath.clear();
|
||||
m_meshRef.Reset();
|
||||
@@ -106,8 +155,30 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldTraceMeshPath(pendingMeshPath) || pendingMeshRef.IsValid()) {
|
||||
TraceMeshFilter(
|
||||
*this,
|
||||
std::string("Deserialize path=") + pendingMeshPath +
|
||||
" ref=" + EncodeAssetRef(pendingMeshRef) +
|
||||
" deferred=" + (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled() ? "1" : "0"));
|
||||
}
|
||||
|
||||
if (pendingMeshRef.IsValid()) {
|
||||
m_meshRef = pendingMeshRef;
|
||||
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||
Containers::String resolvedPath;
|
||||
if (Resources::ResourceManager::Get().TryResolveAssetPath(pendingMeshRef, resolvedPath)) {
|
||||
m_meshPath = ToStdString(resolvedPath);
|
||||
if (ShouldTraceMeshPath(m_meshPath)) {
|
||||
TraceMeshFilter(*this, std::string("Resolved meshRef to path=") + m_meshPath);
|
||||
}
|
||||
BeginAsyncMeshLoad(m_meshPath);
|
||||
return;
|
||||
}
|
||||
|
||||
TraceMeshFilter(*this, std::string("Failed to resolve meshRef, fallback path=") + pendingMeshPath);
|
||||
}
|
||||
|
||||
m_mesh = Resources::ResourceManager::Get().Load<Resources::Mesh>(pendingMeshRef);
|
||||
if (m_mesh.Get() != nullptr) {
|
||||
m_meshPath = ToStdString(m_mesh->GetPath());
|
||||
@@ -115,9 +186,79 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
|
||||
m_meshPath = pendingMeshPath;
|
||||
}
|
||||
} else if (!pendingMeshPath.empty()) {
|
||||
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||
m_meshPath = pendingMeshPath;
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
||||
m_meshRef.Reset();
|
||||
}
|
||||
BeginAsyncMeshLoad(m_meshPath);
|
||||
return;
|
||||
}
|
||||
|
||||
SetMeshPath(pendingMeshPath);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFilterComponent::BeginAsyncMeshLoad(const std::string& meshPath) {
|
||||
if (meshPath.empty()) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_mesh.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
m_mesh.Reset();
|
||||
m_pendingMeshLoad = std::make_shared<PendingMeshLoadState>();
|
||||
if (ShouldTraceMeshPath(meshPath)) {
|
||||
TraceMeshFilter(*this, std::string("BeginAsyncMeshLoad path=") + meshPath);
|
||||
}
|
||||
std::weak_ptr<PendingMeshLoadState> weakState = m_pendingMeshLoad;
|
||||
Resources::ResourceManager::Get().LoadAsync(meshPath.c_str(),
|
||||
Resources::ResourceType::Mesh,
|
||||
[weakState](Resources::LoadResult result) {
|
||||
if (std::shared_ptr<PendingMeshLoadState> state = weakState.lock()) {
|
||||
state->result = std::move(result);
|
||||
state->completed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MeshFilterComponent::ResolvePendingMesh() {
|
||||
if (!m_pendingMeshLoad || !m_pendingMeshLoad->completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<PendingMeshLoadState> completedLoad = std::move(m_pendingMeshLoad);
|
||||
m_pendingMeshLoad.reset();
|
||||
|
||||
if (!completedLoad->result || completedLoad->result.resource == nullptr) {
|
||||
if (ShouldTraceMeshPath(m_meshPath)) {
|
||||
TraceMeshFilter(
|
||||
*this,
|
||||
std::string("ResolvePendingMesh failed path=") + m_meshPath +
|
||||
" error=" + ToStdString(completedLoad->result.errorMessage));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_mesh = Resources::ResourceHandle<Resources::Mesh>(
|
||||
static_cast<Resources::Mesh*>(completedLoad->result.resource));
|
||||
if (m_mesh.Get() == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_meshPath = ToStdString(m_mesh->GetPath());
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
||||
m_meshRef.Reset();
|
||||
}
|
||||
|
||||
if (ShouldTraceMeshPath(m_meshPath)) {
|
||||
TraceMeshFilter(
|
||||
*this,
|
||||
std::string("ResolvePendingMesh success path=") + m_meshPath +
|
||||
" ref=" + EncodeAssetRef(m_meshRef) +
|
||||
" vertices=" + std::to_string(m_mesh->GetVertexCount()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Components
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "Components/MeshRendererComponent.h"
|
||||
|
||||
#include "Components/GameObject.h"
|
||||
#include "Core/Asset/ResourceManager.h"
|
||||
#include "Debug/Logger.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
@@ -13,6 +15,12 @@ std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
bool ShouldTraceMaterialPath(const std::string& path) {
|
||||
return path.rfind("builtin://", 0) == 0 ||
|
||||
path.find("backpack") != std::string::npos ||
|
||||
path.find("New Material.mat") != std::string::npos;
|
||||
}
|
||||
|
||||
std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
|
||||
if (!assetRef.IsValid()) {
|
||||
return std::string();
|
||||
@@ -83,13 +91,66 @@ std::vector<Resources::AssetRef> SplitMaterialRefs(const std::string& value) {
|
||||
return refs;
|
||||
}
|
||||
|
||||
std::string JoinMaterialPaths(const std::vector<std::string>& values) {
|
||||
std::ostringstream stream;
|
||||
for (size_t index = 0; index < values.size(); ++index) {
|
||||
if (index > 0) {
|
||||
stream << "|";
|
||||
}
|
||||
stream << values[index];
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string JoinMaterialRefs(const std::vector<Resources::AssetRef>& values) {
|
||||
std::ostringstream stream;
|
||||
for (size_t index = 0; index < values.size(); ++index) {
|
||||
if (index > 0) {
|
||||
stream << "|";
|
||||
}
|
||||
stream << EncodeAssetRef(values[index]);
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ShouldTraceAnyMaterialPath(const std::vector<std::string>& values) {
|
||||
for (const std::string& value : values) {
|
||||
if (ShouldTraceMaterialPath(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string DescribeOwner(const MeshRendererComponent& component) {
|
||||
const GameObject* owner = component.GetGameObject();
|
||||
if (owner == nullptr) {
|
||||
return "<no-owner>";
|
||||
}
|
||||
|
||||
return std::string("id=") + std::to_string(owner->GetID()) + ",name=" + owner->GetName();
|
||||
}
|
||||
|
||||
void TraceMeshRenderer(const MeshRendererComponent& component, const std::string& message) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String(("[MeshRenderer] owner={" + DescribeOwner(component) + "} " + message).c_str()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct MeshRendererComponent::PendingMaterialLoadState {
|
||||
Resources::LoadResult result;
|
||||
bool completed = false;
|
||||
};
|
||||
|
||||
Resources::Material* MeshRendererComponent::GetMaterial(size_t index) const {
|
||||
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
|
||||
return index < m_materials.size() ? m_materials[index].Get() : nullptr;
|
||||
}
|
||||
|
||||
const Resources::ResourceHandle<Resources::Material>& MeshRendererComponent::GetMaterialHandle(size_t index) const {
|
||||
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
|
||||
static const Resources::ResourceHandle<Resources::Material> kNullHandle;
|
||||
return index < m_materials.size() ? m_materials[index] : kNullHandle;
|
||||
}
|
||||
@@ -101,6 +162,7 @@ const std::string& MeshRendererComponent::GetMaterialPath(size_t index) const {
|
||||
|
||||
void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& materialPath) {
|
||||
EnsureMaterialSlot(index);
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
m_materialPaths[index] = materialPath;
|
||||
if (materialPath.empty()) {
|
||||
m_materials[index].Reset();
|
||||
@@ -112,10 +174,20 @@ void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& mat
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(materialPath.c_str(), Resources::ResourceType::Material, m_materialRefs[index])) {
|
||||
m_materialRefs[index].Reset();
|
||||
}
|
||||
|
||||
if (ShouldTraceMaterialPath(materialPath)) {
|
||||
TraceMeshRenderer(
|
||||
*this,
|
||||
std::string("SetMaterialPath slot=") + std::to_string(index) +
|
||||
" path=" + materialPath +
|
||||
" ref=" + EncodeAssetRef(m_materialRefs[index]) +
|
||||
" loaded=" + (m_materials[index].Get() != nullptr ? "1" : "0"));
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material) {
|
||||
EnsureMaterialSlot(index);
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
m_materials[index] = material;
|
||||
m_materialPaths[index] = MaterialPathFromHandle(material);
|
||||
if (m_materialPaths[index].empty() ||
|
||||
@@ -132,6 +204,8 @@ void MeshRendererComponent::SetMaterials(const std::vector<Resources::ResourceHa
|
||||
m_materials = materials;
|
||||
m_materialPaths.resize(materials.size());
|
||||
m_materialRefs.resize(materials.size());
|
||||
m_pendingMaterialLoads.clear();
|
||||
m_pendingMaterialLoads.resize(materials.size());
|
||||
for (size_t i = 0; i < materials.size(); ++i) {
|
||||
m_materialPaths[i] = MaterialPathFromHandle(materials[i]);
|
||||
if (m_materialPaths[i].empty() ||
|
||||
@@ -145,6 +219,7 @@ void MeshRendererComponent::ClearMaterials() {
|
||||
m_materials.clear();
|
||||
m_materialPaths.clear();
|
||||
m_materialRefs.clear();
|
||||
m_pendingMaterialLoads.clear();
|
||||
}
|
||||
|
||||
void MeshRendererComponent::Serialize(std::ostream& os) const {
|
||||
@@ -194,6 +269,7 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
|
||||
m_materialPaths = SplitMaterialPaths(value);
|
||||
m_materials.resize(m_materialPaths.size());
|
||||
m_materialRefs.resize(m_materialPaths.size());
|
||||
m_pendingMaterialLoads.resize(m_materialPaths.size());
|
||||
} else if (key == "materialRefs") {
|
||||
pendingMaterialRefs = SplitMaterialRefs(value);
|
||||
} else if (key == "castShadows") {
|
||||
@@ -209,28 +285,149 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
|
||||
m_materialPaths.resize(pendingMaterialRefs.size());
|
||||
m_materials.resize(pendingMaterialRefs.size());
|
||||
m_materialRefs.resize(pendingMaterialRefs.size());
|
||||
m_pendingMaterialLoads.resize(pendingMaterialRefs.size());
|
||||
}
|
||||
|
||||
if (ShouldTraceAnyMaterialPath(m_materialPaths)) {
|
||||
TraceMeshRenderer(
|
||||
*this,
|
||||
std::string("Deserialize paths=") + JoinMaterialPaths(m_materialPaths) +
|
||||
" refs=" + JoinMaterialRefs(pendingMaterialRefs) +
|
||||
" deferred=" + (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled() ? "1" : "0"));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_materialPaths.size(); ++i) {
|
||||
bool restoredOrQueued = false;
|
||||
if (i < pendingMaterialRefs.size() && pendingMaterialRefs[i].IsValid()) {
|
||||
m_materialRefs[i] = pendingMaterialRefs[i];
|
||||
m_materials[i] = Resources::ResourceManager::Get().Load<Resources::Material>(pendingMaterialRefs[i]);
|
||||
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||
Containers::String resolvedPath;
|
||||
if (Resources::ResourceManager::Get().TryResolveAssetPath(pendingMaterialRefs[i], resolvedPath)) {
|
||||
m_materialPaths[i] = ToStdString(resolvedPath);
|
||||
if (ShouldTraceMaterialPath(m_materialPaths[i])) {
|
||||
TraceMeshRenderer(
|
||||
*this,
|
||||
std::string("Resolved materialRef slot=") + std::to_string(i) +
|
||||
" path=" + m_materialPaths[i]);
|
||||
}
|
||||
BeginAsyncMaterialLoad(i, m_materialPaths[i]);
|
||||
restoredOrQueued = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!restoredOrQueued) {
|
||||
m_materials[i] = Resources::ResourceManager::Get().Load<Resources::Material>(pendingMaterialRefs[i]);
|
||||
}
|
||||
if (m_materials[i].Get() != nullptr) {
|
||||
m_materialPaths[i] = MaterialPathFromHandle(m_materials[i]);
|
||||
restoredOrQueued = true;
|
||||
}
|
||||
|
||||
if (restoredOrQueued) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||
if (!m_materialPaths[i].empty()) {
|
||||
if (!m_materialRefs[i].IsValid() &&
|
||||
!Resources::ResourceManager::Get().TryGetAssetRef(m_materialPaths[i].c_str(),
|
||||
Resources::ResourceType::Material,
|
||||
m_materialRefs[i])) {
|
||||
m_materialRefs[i].Reset();
|
||||
}
|
||||
BeginAsyncMaterialLoad(i, m_materialPaths[i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
SetMaterialPath(i, m_materialPaths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::BeginAsyncMaterialLoad(size_t index, const std::string& materialPath) {
|
||||
EnsureMaterialSlot(index);
|
||||
if (materialPath.empty()) {
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
m_materials[index].Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
m_materials[index].Reset();
|
||||
m_pendingMaterialLoads[index] = std::make_shared<PendingMaterialLoadState>();
|
||||
if (ShouldTraceMaterialPath(materialPath)) {
|
||||
TraceMeshRenderer(
|
||||
*this,
|
||||
std::string("BeginAsyncMaterialLoad slot=") + std::to_string(index) +
|
||||
" path=" + materialPath);
|
||||
}
|
||||
std::weak_ptr<PendingMaterialLoadState> weakState = m_pendingMaterialLoads[index];
|
||||
Resources::ResourceManager::Get().LoadAsync(materialPath.c_str(),
|
||||
Resources::ResourceType::Material,
|
||||
[weakState](Resources::LoadResult result) {
|
||||
if (std::shared_ptr<PendingMaterialLoadState> state = weakState.lock()) {
|
||||
state->result = std::move(result);
|
||||
state->completed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MeshRendererComponent::ResolvePendingMaterials() {
|
||||
for (size_t index = 0; index < m_pendingMaterialLoads.size(); ++index) {
|
||||
if (!m_pendingMaterialLoads[index] || !m_pendingMaterialLoads[index]->completed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::shared_ptr<PendingMaterialLoadState> completedLoad = std::move(m_pendingMaterialLoads[index]);
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
|
||||
if (!completedLoad->result || completedLoad->result.resource == nullptr) {
|
||||
if (ShouldTraceMaterialPath(m_materialPaths[index])) {
|
||||
TraceMeshRenderer(
|
||||
*this,
|
||||
std::string("ResolvePendingMaterial failed slot=") + std::to_string(index) +
|
||||
" path=" + m_materialPaths[index] +
|
||||
" error=" + ToStdString(completedLoad->result.errorMessage));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
m_materials[index] = Resources::ResourceHandle<Resources::Material>(
|
||||
static_cast<Resources::Material*>(completedLoad->result.resource));
|
||||
if (m_materials[index].Get() == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_materialPaths[index] = MaterialPathFromHandle(m_materials[index]);
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_materialPaths[index].c_str(),
|
||||
Resources::ResourceType::Material,
|
||||
m_materialRefs[index])) {
|
||||
m_materialRefs[index].Reset();
|
||||
}
|
||||
|
||||
if (ShouldTraceMaterialPath(m_materialPaths[index])) {
|
||||
TraceMeshRenderer(
|
||||
*this,
|
||||
std::string("ResolvePendingMaterial success slot=") + std::to_string(index) +
|
||||
" path=" + m_materialPaths[index] +
|
||||
" ref=" + EncodeAssetRef(m_materialRefs[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::EnsureMaterialSlot(size_t index) {
|
||||
if (index >= m_materials.size()) {
|
||||
m_materials.resize(index + 1);
|
||||
}
|
||||
if (index >= m_materialPaths.size()) {
|
||||
m_materialPaths.resize(index + 1);
|
||||
}
|
||||
if (index >= m_materialRefs.size()) {
|
||||
m_materialRefs.resize(index + 1);
|
||||
}
|
||||
if (index >= m_pendingMaterialLoads.size()) {
|
||||
m_pendingMaterialLoads.resize(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
std::string MeshRendererComponent::MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
|
||||
@@ -23,6 +24,17 @@ std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
bool ShouldTraceAssetPath(const Containers::String& path) {
|
||||
const std::string text = ToStdString(path);
|
||||
return text.rfind("builtin://", 0) == 0 ||
|
||||
text.find("backpack") != std::string::npos ||
|
||||
text.find("New Material.mat") != std::string::npos;
|
||||
}
|
||||
|
||||
bool HasVirtualPathScheme(const Containers::String& value) {
|
||||
return ToStdString(value).find("://") != std::string::npos;
|
||||
}
|
||||
|
||||
Containers::String ToContainersString(const std::string& value) {
|
||||
return Containers::String(value.c_str());
|
||||
}
|
||||
@@ -368,6 +380,10 @@ bool AssetDatabase::ResolvePath(const Containers::String& requestPath,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HasVirtualPathScheme(requestPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path inputPath(requestPath.CStr());
|
||||
if (inputPath.is_absolute()) {
|
||||
outAbsolutePath = NormalizePathString(inputPath);
|
||||
@@ -863,11 +879,24 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
Containers::String absolutePath;
|
||||
Containers::String relativePath;
|
||||
if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) {
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[AssetDatabase] EnsureArtifact unresolved path=") + requestPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path absoluteFsPath(absolutePath.CStr());
|
||||
if (!fs::exists(absoluteFsPath)) {
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[AssetDatabase] EnsureArtifact missing source path=") +
|
||||
requestPath +
|
||||
" absolute=" +
|
||||
absolutePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -876,8 +905,31 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[AssetDatabase] EnsureArtifact source path=") +
|
||||
requestPath +
|
||||
" guid=" +
|
||||
sourceRecord.guid.ToString() +
|
||||
" importer=" +
|
||||
sourceRecord.importerName +
|
||||
" relative=" +
|
||||
sourceRecord.relativePath);
|
||||
}
|
||||
|
||||
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
|
||||
if (primaryType == ResourceType::Unknown || primaryType != requestedType) {
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[AssetDatabase] EnsureArtifact type-mismatch path=") +
|
||||
requestPath +
|
||||
" requested=" +
|
||||
GetResourceTypeName(requestedType) +
|
||||
" importerType=" +
|
||||
GetResourceTypeName(primaryType));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -888,8 +940,18 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
}
|
||||
|
||||
if (ShouldReimport(sourceRecord, artifactRecord)) {
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[AssetDatabase] EnsureArtifact reimport path=") + requestPath);
|
||||
}
|
||||
ArtifactRecord rebuiltRecord;
|
||||
if (!ImportAsset(sourceRecord, rebuiltRecord)) {
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[AssetDatabase] EnsureArtifact reimport failed path=") + requestPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -914,6 +976,17 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
||||
outAsset.artifactDirectory = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->artifactDirectory.CStr());
|
||||
outAsset.artifactMainPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr());
|
||||
outAsset.mainLocalID = artifactRecord->mainLocalID;
|
||||
|
||||
if (ShouldTraceAssetPath(requestPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[AssetDatabase] EnsureArtifact ready path=") +
|
||||
requestPath +
|
||||
" artifactKey=" +
|
||||
artifactRecord->artifactKey +
|
||||
" artifact=" +
|
||||
outAsset.artifactMainPath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
#include <XCEngine/Core/Asset/AsyncLoader.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
bool ShouldTraceAsyncPath(const Containers::String& path) {
|
||||
const std::string text = ToStdString(path);
|
||||
return text.rfind("builtin://", 0) == 0 ||
|
||||
text.find("backpack") != std::string::npos ||
|
||||
text.find("New Material.mat") != std::string::npos;
|
||||
}
|
||||
|
||||
void TraceAsyncLoad(const Containers::String& message) {
|
||||
Debug::Logger::Get().Info(Debug::LogCategory::FileSystem, message);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Core::uint64 LoadRequest::GenerateRequestId() {
|
||||
static std::atomic<Core::uint64> s_requestId{0};
|
||||
return ++s_requestId;
|
||||
@@ -16,11 +36,43 @@ AsyncLoader& AsyncLoader::Get() {
|
||||
}
|
||||
|
||||
void AsyncLoader::Initialize(Core::uint32 workerThreadCount) {
|
||||
(void)workerThreadCount;
|
||||
if (m_running.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workerThreadCount == 0) {
|
||||
workerThreadCount = 1;
|
||||
}
|
||||
|
||||
m_workerThreads.reserve(workerThreadCount);
|
||||
for (Core::uint32 index = 0; index < workerThreadCount; ++index) {
|
||||
m_workerThreads.emplace_back(&AsyncLoader::WorkerThread, this);
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncLoader::Shutdown() {
|
||||
CancelAll();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_queueMutex);
|
||||
m_running = false;
|
||||
m_pendingQueue.clear();
|
||||
m_pendingCount = 0;
|
||||
}
|
||||
m_pendingCondition.notify_all();
|
||||
|
||||
for (std::thread& workerThread : m_workerThreads) {
|
||||
if (workerThread.joinable()) {
|
||||
workerThread.join();
|
||||
}
|
||||
}
|
||||
m_workerThreads.clear();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_completedMutex);
|
||||
m_completedQueue.clear();
|
||||
}
|
||||
|
||||
m_completedCount = 0;
|
||||
m_totalRequested = 0;
|
||||
}
|
||||
|
||||
void AsyncLoader::Submit(const Containers::String& path, ResourceType type,
|
||||
@@ -47,56 +99,185 @@ void AsyncLoader::SubmitInternal(LoadRequest request) {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(m_queueMutex);
|
||||
m_pendingQueue.PushBack(std::move(request));
|
||||
m_pendingCount++;
|
||||
m_totalRequested++;
|
||||
std::lock_guard<std::mutex> lock(m_queueMutex);
|
||||
if (!m_running) {
|
||||
if (request.callback) {
|
||||
request.callback(LoadResult("Async loader is not initialized"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingQueue.emplace_back(std::move(request));
|
||||
++m_pendingCount;
|
||||
++m_totalRequested;
|
||||
|
||||
const LoadRequest& queuedRequest = m_pendingQueue.back();
|
||||
if (ShouldTraceAsyncPath(queuedRequest.path)) {
|
||||
TraceAsyncLoad(
|
||||
Containers::String("[AsyncLoader] submit id=") +
|
||||
Containers::String(std::to_string(queuedRequest.requestId).c_str()) +
|
||||
" type=" +
|
||||
GetResourceTypeName(queuedRequest.type) +
|
||||
" path=" +
|
||||
queuedRequest.path +
|
||||
" pending=" +
|
||||
Containers::String(std::to_string(m_pendingCount.load()).c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
m_pendingCondition.notify_one();
|
||||
}
|
||||
|
||||
void AsyncLoader::Update() {
|
||||
Containers::Array<LoadRequest> completed;
|
||||
std::deque<CompletedLoadRequest> completed;
|
||||
|
||||
{
|
||||
std::lock_guard lock(m_completedMutex);
|
||||
completed = std::move(m_completedQueue);
|
||||
m_completedQueue.Clear();
|
||||
std::lock_guard<std::mutex> lock(m_completedMutex);
|
||||
completed.swap(m_completedQueue);
|
||||
}
|
||||
|
||||
for (auto& request : completed) {
|
||||
m_pendingCount--;
|
||||
for (CompletedLoadRequest& entry : completed) {
|
||||
if (m_pendingCount > 0) {
|
||||
--m_pendingCount;
|
||||
}
|
||||
++m_completedCount;
|
||||
|
||||
if (request.callback) {
|
||||
LoadResult result(true);
|
||||
request.callback(result);
|
||||
if (ShouldTraceAsyncPath(entry.request.path)) {
|
||||
TraceAsyncLoad(
|
||||
Containers::String("[AsyncLoader] dispatch id=") +
|
||||
Containers::String(std::to_string(entry.request.requestId).c_str()) +
|
||||
" type=" +
|
||||
GetResourceTypeName(entry.request.type) +
|
||||
" path=" +
|
||||
entry.request.path +
|
||||
" success=" +
|
||||
Containers::String(entry.result && entry.result.resource != nullptr ? "1" : "0") +
|
||||
" pending=" +
|
||||
Containers::String(std::to_string(m_pendingCount.load()).c_str()) +
|
||||
(entry.result.errorMessage.Empty()
|
||||
? Containers::String()
|
||||
: Containers::String(" error=") + entry.result.errorMessage));
|
||||
}
|
||||
|
||||
if (entry.request.callback) {
|
||||
entry.request.callback(std::move(entry.result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float AsyncLoader::GetProgress() const {
|
||||
if (m_totalRequested == 0) return 1.0f;
|
||||
return static_cast<float>(m_totalRequested - m_pendingCount.load()) / m_totalRequested;
|
||||
const Core::uint64 totalRequested = m_totalRequested.load();
|
||||
if (totalRequested == 0) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(totalRequested - m_pendingCount.load()) /
|
||||
static_cast<float>(totalRequested);
|
||||
}
|
||||
|
||||
void AsyncLoader::CancelAll() {
|
||||
std::lock_guard lock(m_queueMutex);
|
||||
m_pendingQueue.Clear();
|
||||
m_pendingCount = 0;
|
||||
std::lock_guard<std::mutex> lock(m_queueMutex);
|
||||
if (!m_pendingQueue.empty()) {
|
||||
const Core::uint32 queuedCount = static_cast<Core::uint32>(m_pendingQueue.size());
|
||||
m_pendingQueue.clear();
|
||||
if (m_pendingCount >= queuedCount) {
|
||||
m_pendingCount -= queuedCount;
|
||||
} else {
|
||||
m_pendingCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncLoader::Cancel(Core::uint64 requestId) {
|
||||
std::lock_guard lock(m_queueMutex);
|
||||
(void)requestId;
|
||||
std::lock_guard<std::mutex> lock(m_queueMutex);
|
||||
for (auto it = m_pendingQueue.begin(); it != m_pendingQueue.end(); ++it) {
|
||||
if (it->requestId == requestId) {
|
||||
m_pendingQueue.erase(it);
|
||||
if (m_pendingCount > 0) {
|
||||
--m_pendingCount;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IResourceLoader* AsyncLoader::FindLoader(ResourceType type) const {
|
||||
return ResourceManager::Get().GetLoader(type);
|
||||
}
|
||||
|
||||
void AsyncLoader::WorkerThread() {
|
||||
for (;;) {
|
||||
LoadRequest request;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_queueMutex);
|
||||
m_pendingCondition.wait(lock, [this]() {
|
||||
return !m_running || !m_pendingQueue.empty();
|
||||
});
|
||||
|
||||
if (!m_running && m_pendingQueue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pendingQueue.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
request = std::move(m_pendingQueue.front());
|
||||
m_pendingQueue.pop_front();
|
||||
}
|
||||
|
||||
if (ShouldTraceAsyncPath(request.path)) {
|
||||
TraceAsyncLoad(
|
||||
Containers::String("[AsyncLoader] worker-begin id=") +
|
||||
Containers::String(std::to_string(request.requestId).c_str()) +
|
||||
" type=" +
|
||||
GetResourceTypeName(request.type) +
|
||||
" path=" +
|
||||
request.path);
|
||||
}
|
||||
|
||||
LoadResult result;
|
||||
try {
|
||||
result = ResourceManager::Get().LoadResource(request.path, request.type, request.settings);
|
||||
} catch (const std::exception& exception) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("Async load threw exception for resource: ") +
|
||||
request.path +
|
||||
" - " +
|
||||
exception.what());
|
||||
result = LoadResult(
|
||||
Containers::String("Async load threw exception: ") + exception.what());
|
||||
} catch (...) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("Async load threw unknown exception for resource: ") +
|
||||
request.path);
|
||||
result = LoadResult("Async load threw unknown exception");
|
||||
}
|
||||
|
||||
if (ShouldTraceAsyncPath(request.path)) {
|
||||
TraceAsyncLoad(
|
||||
Containers::String("[AsyncLoader] worker-end id=") +
|
||||
Containers::String(std::to_string(request.requestId).c_str()) +
|
||||
" type=" +
|
||||
GetResourceTypeName(request.type) +
|
||||
" path=" +
|
||||
request.path +
|
||||
" success=" +
|
||||
Containers::String(result && result.resource != nullptr ? "1" : "0") +
|
||||
(result.errorMessage.Empty()
|
||||
? Containers::String()
|
||||
: Containers::String(" error=") + result.errorMessage));
|
||||
}
|
||||
|
||||
QueueCompleted(std::move(request), std::move(result));
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncLoader::QueueCompleted(LoadRequest request, LoadResult result) {
|
||||
std::lock_guard lock(m_completedMutex);
|
||||
(void)request;
|
||||
(void)result;
|
||||
std::lock_guard<std::mutex> lock(m_completedMutex);
|
||||
m_completedQueue.emplace_back(std::move(request), std::move(result));
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
|
||||
@@ -6,12 +6,34 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <exception>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
bool ShouldTraceResourcePath(const Containers::String& path) {
|
||||
const std::string text = ToStdString(path);
|
||||
return text.rfind("builtin://", 0) == 0 ||
|
||||
text.find("backpack") != std::string::npos ||
|
||||
text.find("New Material.mat") != std::string::npos;
|
||||
}
|
||||
|
||||
Containers::String EncodeAssetRef(const AssetRef& assetRef) {
|
||||
if (!assetRef.IsValid()) {
|
||||
return Containers::String("<invalid>");
|
||||
}
|
||||
|
||||
return assetRef.assetGuid.ToString() + "," +
|
||||
Containers::String(std::to_string(assetRef.localID).c_str()) + "," +
|
||||
Containers::String(std::to_string(static_cast<int>(assetRef.resourceType)).c_str());
|
||||
}
|
||||
|
||||
template<typename TLoader>
|
||||
void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) {
|
||||
if (manager.GetLoader(loader.GetResourceType()) == nullptr) {
|
||||
@@ -31,6 +53,17 @@ ResourceManager& ResourceManager::Get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
ResourceManager::ScopedDeferredSceneLoad::ScopedDeferredSceneLoad(ResourceManager& manager)
|
||||
: m_manager(&manager) {
|
||||
m_manager->BeginDeferredSceneLoad();
|
||||
}
|
||||
|
||||
ResourceManager::ScopedDeferredSceneLoad::~ScopedDeferredSceneLoad() {
|
||||
if (m_manager != nullptr) {
|
||||
m_manager->EndDeferredSceneLoad();
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceManager::Initialize() {
|
||||
if (m_asyncLoader) {
|
||||
return;
|
||||
@@ -51,11 +84,17 @@ void ResourceManager::Shutdown() {
|
||||
m_asyncLoader->Shutdown();
|
||||
m_asyncLoader.reset();
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
m_assetDatabase.Shutdown();
|
||||
ResourceFileSystem::Get().Shutdown();
|
||||
|
||||
std::lock_guard<std::mutex> inFlightLock(m_inFlightLoadsMutex);
|
||||
m_inFlightLoads.clear();
|
||||
}
|
||||
|
||||
void ResourceManager::SetResourceRoot(const Containers::String& rootPath) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
m_resourceRoot = rootPath;
|
||||
if (!m_resourceRoot.Empty()) {
|
||||
ResourceFileSystem::Get().Initialize(rootPath);
|
||||
@@ -139,31 +178,48 @@ void ResourceManager::AddToCache(ResourceGUID guid, IResource* resource) {
|
||||
}
|
||||
|
||||
void ResourceManager::Unload(ResourceGUID guid) {
|
||||
std::lock_guard lock(m_mutex);
|
||||
IResource* resource = nullptr;
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
|
||||
auto* it = m_resourceCache.Find(guid);
|
||||
if (it != nullptr) {
|
||||
IResource* resource = *it;
|
||||
m_resourceCache.Erase(guid);
|
||||
m_guidToPath.Erase(guid);
|
||||
m_memoryUsage -= resource->GetMemorySize();
|
||||
auto* it = m_resourceCache.Find(guid);
|
||||
if (it != nullptr) {
|
||||
resource = *it;
|
||||
m_resourceCache.Erase(guid);
|
||||
m_guidToPath.Erase(guid);
|
||||
m_memoryUsage -= resource->GetMemorySize();
|
||||
}
|
||||
}
|
||||
|
||||
if (resource != nullptr) {
|
||||
resource->Release();
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceManager::UnloadAll() {
|
||||
std::lock_guard lock(m_mutex);
|
||||
Containers::Array<IResource*> resourcesToRelease;
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
|
||||
const auto cachedResources = m_resourceCache.GetPairs();
|
||||
for (const auto& pair : cachedResources) {
|
||||
if (pair.second != nullptr) {
|
||||
pair.second->Release();
|
||||
const auto cachedResources = m_resourceCache.GetPairs();
|
||||
resourcesToRelease.Reserve(cachedResources.Size());
|
||||
for (const auto& pair : cachedResources) {
|
||||
if (pair.second != nullptr) {
|
||||
resourcesToRelease.PushBack(pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
m_resourceCache.Clear();
|
||||
m_refCounts.Clear();
|
||||
m_guidToPath.Clear();
|
||||
m_memoryUsage = 0;
|
||||
}
|
||||
|
||||
for (IResource* resource : resourcesToRelease) {
|
||||
if (resource != nullptr) {
|
||||
resource->Release();
|
||||
}
|
||||
}
|
||||
m_resourceCache.Clear();
|
||||
m_refCounts.Clear();
|
||||
m_guidToPath.Clear();
|
||||
m_memoryUsage = 0;
|
||||
}
|
||||
|
||||
void ResourceManager::SetMemoryBudget(size_t bytes) {
|
||||
@@ -210,7 +266,28 @@ void ResourceManager::LoadAsync(const Containers::String& path, ResourceType typ
|
||||
void ResourceManager::LoadAsync(const Containers::String& path, ResourceType type,
|
||||
ImportSettings* settings,
|
||||
std::function<void(LoadResult)> callback) {
|
||||
m_asyncLoader->Submit(path, type, settings, callback);
|
||||
if (!m_asyncLoader) {
|
||||
if (callback) {
|
||||
callback(LoadResult("Async loader is not initialized"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_asyncLoader->Submit(path, type, settings, std::move(callback));
|
||||
}
|
||||
|
||||
void ResourceManager::UpdateAsyncLoads() {
|
||||
if (m_asyncLoader) {
|
||||
m_asyncLoader->Update();
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceManager::IsAsyncLoading() const {
|
||||
return m_asyncLoader && m_asyncLoader->IsLoading();
|
||||
}
|
||||
|
||||
Core::uint32 ResourceManager::GetAsyncPendingCount() const {
|
||||
return m_asyncLoader ? m_asyncLoader->GetPendingCount() : 0;
|
||||
}
|
||||
|
||||
void ResourceManager::Unload(const Containers::String& path) {
|
||||
@@ -245,14 +322,24 @@ Containers::Array<Containers::String> ResourceManager::GetResourcePaths() const
|
||||
}
|
||||
|
||||
void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids) {
|
||||
std::lock_guard lock(m_mutex);
|
||||
for (const auto& guid : guids) {
|
||||
auto* it = m_resourceCache.Find(guid);
|
||||
if (it != nullptr) {
|
||||
IResource* resource = *it;
|
||||
m_resourceCache.Erase(guid);
|
||||
m_guidToPath.Erase(guid);
|
||||
m_memoryUsage -= resource->GetMemorySize();
|
||||
Containers::Array<IResource*> resourcesToRelease;
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
resourcesToRelease.Reserve(guids.Size());
|
||||
for (const auto& guid : guids) {
|
||||
auto* it = m_resourceCache.Find(guid);
|
||||
if (it != nullptr) {
|
||||
IResource* resource = *it;
|
||||
m_resourceCache.Erase(guid);
|
||||
m_guidToPath.Erase(guid);
|
||||
m_memoryUsage -= resource->GetMemorySize();
|
||||
resourcesToRelease.PushBack(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (IResource* resource : resourcesToRelease) {
|
||||
if (resource != nullptr) {
|
||||
resource->Release();
|
||||
}
|
||||
}
|
||||
@@ -260,12 +347,62 @@ void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids)
|
||||
|
||||
void ResourceManager::RefreshAssetDatabase() {
|
||||
if (!m_resourceRoot.Empty()) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
m_assetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const {
|
||||
return m_assetDatabase.TryGetAssetRef(path, resourceType, outRef);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
const bool resolved = m_assetDatabase.TryGetAssetRef(path, resourceType, outRef);
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] TryGetAssetRef path=") +
|
||||
path +
|
||||
" type=" +
|
||||
GetResourceTypeName(resourceType) +
|
||||
" success=" +
|
||||
Containers::String(resolved ? "1" : "0") +
|
||||
" ref=" +
|
||||
EncodeAssetRef(outRef));
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
bool ResourceManager::TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) const {
|
||||
if (!assetRef.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
|
||||
const bool resolved = m_assetDatabase.TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
|
||||
if (resolved && ShouldTraceResourcePath(outPath)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] TryResolveAssetPath ref=") +
|
||||
EncodeAssetRef(assetRef) +
|
||||
" path=" +
|
||||
outPath);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
void ResourceManager::BeginDeferredSceneLoad() {
|
||||
++m_deferredSceneLoadDepth;
|
||||
}
|
||||
|
||||
void ResourceManager::EndDeferredSceneLoad() {
|
||||
const Core::uint32 currentDepth = m_deferredSceneLoadDepth.load();
|
||||
if (currentDepth == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
--m_deferredSceneLoadDepth;
|
||||
}
|
||||
|
||||
bool ResourceManager::IsDeferredSceneLoadEnabled() const {
|
||||
return m_deferredSceneLoadDepth.load() > 0;
|
||||
}
|
||||
|
||||
LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
@@ -273,7 +410,23 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
ImportSettings* settings) {
|
||||
const ResourceGUID guid = ResourceGUID::Generate(path);
|
||||
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource request path=") +
|
||||
path +
|
||||
" type=" +
|
||||
GetResourceTypeName(type) +
|
||||
" root=" +
|
||||
m_resourceRoot);
|
||||
}
|
||||
|
||||
if (IResource* cached = FindInCache(guid)) {
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource cache-hit path=") + path);
|
||||
}
|
||||
return LoadResult(cached);
|
||||
}
|
||||
|
||||
@@ -285,24 +438,125 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
return LoadResult(false, "Loader not found");
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
if (!m_resourceRoot.Empty() &&
|
||||
m_assetDatabase.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
loadPath = resolvedAsset.artifactMainPath;
|
||||
const InFlightLoadKey inFlightKey{ guid, type };
|
||||
std::shared_ptr<InFlightLoadState> inFlightState;
|
||||
bool shouldExecuteLoad = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> inFlightLock(m_inFlightLoadsMutex);
|
||||
auto inFlightIt = m_inFlightLoads.find(inFlightKey);
|
||||
if (inFlightIt == m_inFlightLoads.end()) {
|
||||
inFlightState = std::make_shared<InFlightLoadState>();
|
||||
m_inFlightLoads.emplace(inFlightKey, inFlightState);
|
||||
shouldExecuteLoad = true;
|
||||
} else {
|
||||
inFlightState = inFlightIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
auto completeInFlightLoad = [&](const LoadResult& result) {
|
||||
{
|
||||
std::lock_guard<std::mutex> inFlightLock(m_inFlightLoadsMutex);
|
||||
inFlightState->completed = true;
|
||||
inFlightState->success = result && result.resource != nullptr;
|
||||
inFlightState->errorMessage = result.errorMessage;
|
||||
m_inFlightLoads.erase(inFlightKey);
|
||||
}
|
||||
inFlightState->condition.notify_all();
|
||||
};
|
||||
|
||||
if (!shouldExecuteLoad) {
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource wait-inflight path=") +
|
||||
path +
|
||||
" type=" +
|
||||
GetResourceTypeName(type));
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> inFlightLock(m_inFlightLoadsMutex);
|
||||
inFlightState->condition.wait(inFlightLock, [&inFlightState]() {
|
||||
return inFlightState->completed;
|
||||
});
|
||||
}
|
||||
|
||||
if (IResource* cached = FindInCache(guid)) {
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource in-flight-cache-hit path=") + path);
|
||||
}
|
||||
return LoadResult(cached);
|
||||
}
|
||||
|
||||
return LoadResult(
|
||||
!inFlightState->errorMessage.Empty()
|
||||
? inFlightState->errorMessage
|
||||
: Containers::String("In-flight load completed without cached resource"));
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> ioLock(m_ioMutex);
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
if (!m_resourceRoot.Empty() &&
|
||||
m_assetDatabase.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
loadPath = resolvedAsset.artifactMainPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource artifact path=") +
|
||||
path +
|
||||
" artifact=" +
|
||||
loadPath);
|
||||
}
|
||||
} else if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource direct path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
}
|
||||
|
||||
LoadResult result;
|
||||
try {
|
||||
result = loader->Load(loadPath, settings);
|
||||
} catch (const std::exception& exception) {
|
||||
result = LoadResult(
|
||||
Containers::String("LoadResource exception: ") +
|
||||
Containers::String(exception.what()));
|
||||
} catch (...) {
|
||||
result = LoadResult("LoadResource exception: unknown");
|
||||
}
|
||||
|
||||
LoadResult result = loader->Load(loadPath, settings);
|
||||
if (!result || result.resource == nullptr) {
|
||||
Debug::Logger::Get().Error(Debug::LogCategory::FileSystem,
|
||||
Containers::String("Failed to load resource: ") + path + " - " + result.errorMessage);
|
||||
completeInFlightLoad(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource success path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
|
||||
result.resource->m_path = path;
|
||||
AddToCache(guid, result.resource);
|
||||
m_guidToPath.Insert(guid, path);
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
m_guidToPath.Insert(guid, path);
|
||||
}
|
||||
completeInFlightLoad(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Scene/Scene.h"
|
||||
#include "Components/ComponentFactoryRegistry.h"
|
||||
#include "Debug/Logger.h"
|
||||
#include "Components/GameObject.h"
|
||||
#include "Components/TransformComponent.h"
|
||||
#include <sstream>
|
||||
@@ -11,6 +12,16 @@ namespace Components {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ShouldTraceSceneComponentPayload(const std::string& payload) {
|
||||
return payload.find("builtin://") != std::string::npos ||
|
||||
payload.find("backpack") != std::string::npos ||
|
||||
payload.find("New Material.mat") != std::string::npos;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
|
||||
struct PendingComponentData {
|
||||
std::string type;
|
||||
std::string payload;
|
||||
@@ -338,6 +349,19 @@ void Scene::DeserializeFromString(const std::string& data) {
|
||||
}
|
||||
|
||||
for (const PendingComponentData& componentData : pending.components) {
|
||||
if (ShouldTraceSceneComponentPayload(componentData.payload)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[Scene] Deserialize component objectId=") +
|
||||
Containers::String(std::to_string(pending.id).c_str()) +
|
||||
" name=" +
|
||||
Containers::String(pending.name.c_str()) +
|
||||
" type=" +
|
||||
Containers::String(componentData.type.c_str()) +
|
||||
" payload=" +
|
||||
Containers::String(componentData.payload.c_str()));
|
||||
}
|
||||
|
||||
if (Component* component = ComponentFactoryRegistry::Get().CreateComponent(go.get(), componentData.type)) {
|
||||
if (!componentData.payload.empty()) {
|
||||
std::istringstream componentStream(componentData.payload);
|
||||
@@ -396,11 +420,24 @@ std::string Scene::SerializeToString() const {
|
||||
void Scene::Load(const std::string& filePath) {
|
||||
std::ifstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
Debug::Logger::Get().Warning(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[Scene] Load failed to open file=") + Containers::String(filePath.c_str()));
|
||||
return;
|
||||
}
|
||||
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[Scene] Load file=") + Containers::String(filePath.c_str()));
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[Scene] Load bytes=") +
|
||||
Containers::String(std::to_string(buffer.str().size()).c_str()) +
|
||||
" file=" +
|
||||
Containers::String(filePath.c_str()));
|
||||
DeserializeFromString(buffer.str());
|
||||
}
|
||||
|
||||
|
||||
@@ -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