diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index 29ef5b0f..ddc8ccbd 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -9,7 +9,9 @@ #include "UI/BuiltInIcons.h" #include "Platform/Win32Utf8.h" #include "Platform/WindowsProcessDiagnostics.h" +#include #include +#include #include 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(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(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(); diff --git a/editor/src/Application.h b/editor/src/Application.h index e2063533..4747beba 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -6,6 +6,7 @@ #include "Viewport/ViewportHostService.h" #include +#include #include #include #include @@ -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; }; } diff --git a/editor/src/Core/EditorWorkspace.h b/editor/src/Core/EditorWorkspace.h index f6ffe582..95b245dc 100644 --- a/editor/src/Core/EditorWorkspace.h +++ b/editor/src/Core/EditorWorkspace.h @@ -12,6 +12,8 @@ #include "panels/ProjectPanel.h" #include "panels/SceneViewPanel.h" +#include + #include #include #include @@ -53,6 +55,7 @@ public: } void Update(float dt) { + ::XCEngine::Resources::ResourceManager::Get().UpdateAsyncLoads(); m_panels.UpdateAll(dt); } diff --git a/editor/src/Managers/SceneManager.cpp b/editor/src/Managers/SceneManager.cpp index d2e2c3e4..6c64b3f7 100644 --- a/editor/src/Managers/SceneManager.cpp +++ b/editor/src/Managers/SceneManager.cpp @@ -2,6 +2,7 @@ #include "Core/EventBus.h" #include "Core/EditorEvents.h" #include "Utils/ProjectFileUtils.h" +#include #include #include #include @@ -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(); diff --git a/engine/include/XCEngine/Components/MeshFilterComponent.h b/engine/include/XCEngine/Components/MeshFilterComponent.h index 9e2dfeca..e883eccf 100644 --- a/engine/include/XCEngine/Components/MeshFilterComponent.h +++ b/engine/include/XCEngine/Components/MeshFilterComponent.h @@ -5,6 +5,7 @@ #include #include +#include #include 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& GetMeshHandle() const { return m_mesh; } + Resources::Mesh* GetMesh() const; + const Resources::ResourceHandle& 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 m_mesh; std::string m_meshPath; Resources::AssetRef m_meshRef; + std::shared_ptr m_pendingMeshLoad; }; } // namespace Components diff --git a/engine/include/XCEngine/Components/MeshRendererComponent.h b/engine/include/XCEngine/Components/MeshRendererComponent.h index 002241a3..efc0e2dc 100644 --- a/engine/include/XCEngine/Components/MeshRendererComponent.h +++ b/engine/include/XCEngine/Components/MeshRendererComponent.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -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& material); std::vector> m_materials; std::vector m_materialPaths; std::vector m_materialRefs; + std::vector> m_pendingMaterialLoads; bool m_castShadows = true; bool m_receiveShadows = true; uint32_t m_renderLayer = 0; diff --git a/engine/include/XCEngine/Core/Asset/AsyncLoader.h b/engine/include/XCEngine/Core/Asset/AsyncLoader.h index 253e5c29..7a18583d 100644 --- a/engine/include/XCEngine/Core/Asset/AsyncLoader.h +++ b/engine/include/XCEngine/Core/Asset/AsyncLoader.h @@ -2,10 +2,13 @@ #include #include -#include -#include #include +#include +#include #include +#include +#include +#include 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 m_pendingQueue; - - Threading::Mutex m_completedMutex; - Containers::Array m_completedQueue; - + + std::mutex m_queueMutex; + std::condition_variable m_pendingCondition; + std::deque m_pendingQueue; + + std::mutex m_completedMutex; + std::deque m_completedQueue; + std::vector m_workerThreads; + + std::atomic m_running{false}; std::atomic m_pendingCount{0}; std::atomic m_completedCount{0}; - Core::uint32 m_totalRequested = 0; + std::atomic m_totalRequested{0}; }; } // namespace Resources diff --git a/engine/include/XCEngine/Core/Asset/ResourceManager.h b/engine/include/XCEngine/Core/Asset/ResourceManager.h index 44b132c3..094770db 100644 --- a/engine/include/XCEngine/Core/Asset/ResourceManager.h +++ b/engine/include/XCEngine/Core/Asset/ResourceManager.h @@ -11,6 +11,11 @@ #include #include #include +#include +#include +#include +#include +#include #include 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, "T must derive from IResource"); Containers::String path; - if (!assetRef.IsValid() || !m_assetDatabase.TryGetPrimaryAssetPath(assetRef.assetGuid, path)) { + if (!TryResolveAssetPath(assetRef, path)) { return ResourceHandle(); } @@ -54,6 +70,9 @@ public: std::function callback); void LoadAsync(const Containers::String& path, ResourceType type, ImportSettings* settings, std::function 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& 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{}(key.guid) ^ + (static_cast(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 m_asyncLoader; Threading::Mutex m_mutex; + mutable std::recursive_mutex m_ioMutex; + std::mutex m_inFlightLoadsMutex; + std::unordered_map, InFlightLoadKeyHasher> m_inFlightLoads; + std::atomic m_deferredSceneLoadDepth{0}; friend class ResourceHandleBase; + friend class AsyncLoader; }; } // namespace Resources diff --git a/engine/src/Components/MeshFilterComponent.cpp b/engine/src/Components/MeshFilterComponent.cpp index 5abcaf39..74e0b500 100644 --- a/engine/src/Components/MeshFilterComponent.cpp +++ b/engine/src/Components/MeshFilterComponent.cpp @@ -1,6 +1,8 @@ #include "Components/MeshFilterComponent.h" +#include "Components/GameObject.h" #include "Core/Asset/ResourceManager.h" +#include "Debug/Logger.h" #include @@ -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 ""; + } + + 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(this)->ResolvePendingMesh(); + return m_mesh.Get(); +} + +const Resources::ResourceHandle& MeshFilterComponent::GetMeshHandle() const { + const_cast(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& 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(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(); + if (ShouldTraceMeshPath(meshPath)) { + TraceMeshFilter(*this, std::string("BeginAsyncMeshLoad path=") + meshPath); + } + std::weak_ptr weakState = m_pendingMeshLoad; + Resources::ResourceManager::Get().LoadAsync(meshPath.c_str(), + Resources::ResourceType::Mesh, + [weakState](Resources::LoadResult result) { + if (std::shared_ptr state = weakState.lock()) { + state->result = std::move(result); + state->completed = true; + } + }); +} + +void MeshFilterComponent::ResolvePendingMesh() { + if (!m_pendingMeshLoad || !m_pendingMeshLoad->completed) { + return; + } + + std::shared_ptr 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( + static_cast(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 diff --git a/engine/src/Components/MeshRendererComponent.cpp b/engine/src/Components/MeshRendererComponent.cpp index b92c214d..bfd2018a 100644 --- a/engine/src/Components/MeshRendererComponent.cpp +++ b/engine/src/Components/MeshRendererComponent.cpp @@ -1,6 +1,8 @@ #include "Components/MeshRendererComponent.h" +#include "Components/GameObject.h" #include "Core/Asset/ResourceManager.h" +#include "Debug/Logger.h" #include @@ -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 SplitMaterialRefs(const std::string& value) { return refs; } +std::string JoinMaterialPaths(const std::vector& 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& 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& 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 ""; + } + + 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(this)->ResolvePendingMaterials(); return index < m_materials.size() ? m_materials[index].Get() : nullptr; } const Resources::ResourceHandle& MeshRendererComponent::GetMaterialHandle(size_t index) const { + const_cast(this)->ResolvePendingMaterials(); static const Resources::ResourceHandle 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& 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(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(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(); + if (ShouldTraceMaterialPath(materialPath)) { + TraceMeshRenderer( + *this, + std::string("BeginAsyncMaterialLoad slot=") + std::to_string(index) + + " path=" + materialPath); + } + std::weak_ptr weakState = m_pendingMaterialLoads[index]; + Resources::ResourceManager::Get().LoadAsync(materialPath.c_str(), + Resources::ResourceType::Material, + [weakState](Resources::LoadResult result) { + if (std::shared_ptr 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 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( + static_cast(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& material) { diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index efc2c07a..7f495c57 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -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; } diff --git a/engine/src/Core/Asset/AsyncLoader.cpp b/engine/src/Core/Asset/AsyncLoader.cpp index e3d62b3b..c818054c 100644 --- a/engine/src/Core/Asset/AsyncLoader.cpp +++ b/engine/src/Core/Asset/AsyncLoader.cpp @@ -1,10 +1,30 @@ #include #include #include +#include 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 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 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 lock(m_completedMutex); + m_completedQueue.clear(); + } + + m_completedCount = 0; + m_totalRequested = 0; } void AsyncLoader::Submit(const Containers::String& path, ResourceType type, @@ -36,7 +88,7 @@ void AsyncLoader::Submit(const Containers::String& path, ResourceType type, Impo void AsyncLoader::SubmitInternal(LoadRequest request) { IResourceLoader* loader = FindLoader(request.type); - + if (!loader) { if (request.callback) { LoadResult result(Containers::String("No loader for type: ") + @@ -45,58 +97,187 @@ void AsyncLoader::SubmitInternal(LoadRequest request) { } return; } - + { - std::lock_guard lock(m_queueMutex); - m_pendingQueue.PushBack(std::move(request)); - m_pendingCount++; - m_totalRequested++; + std::lock_guard 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 completed; - + std::deque completed; + { - std::lock_guard lock(m_completedMutex); - completed = std::move(m_completedQueue); - m_completedQueue.Clear(); + std::lock_guard lock(m_completedMutex); + completed.swap(m_completedQueue); } - - for (auto& request : completed) { - m_pendingCount--; - - if (request.callback) { - LoadResult result(true); - request.callback(result); + + for (CompletedLoadRequest& entry : completed) { + if (m_pendingCount > 0) { + --m_pendingCount; + } + ++m_completedCount; + + 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(m_totalRequested - m_pendingCount.load()) / m_totalRequested; + const Core::uint64 totalRequested = m_totalRequested.load(); + if (totalRequested == 0) { + return 1.0f; + } + + return static_cast(totalRequested - m_pendingCount.load()) / + static_cast(totalRequested); } void AsyncLoader::CancelAll() { - std::lock_guard lock(m_queueMutex); - m_pendingQueue.Clear(); - m_pendingCount = 0; + std::lock_guard lock(m_queueMutex); + if (!m_pendingQueue.empty()) { + const Core::uint32 queuedCount = static_cast(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 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 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 lock(m_completedMutex); + m_completedQueue.emplace_back(std::move(request), std::move(result)); } } // namespace Resources diff --git a/engine/src/Core/Asset/ResourceManager.cpp b/engine/src/Core/Asset/ResourceManager.cpp index be80ce8b..8876121b 100644 --- a/engine/src/Core/Asset/ResourceManager.cpp +++ b/engine/src/Core/Asset/ResourceManager.cpp @@ -6,12 +6,34 @@ #include #include #include +#include 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(""); + } + + return assetRef.assetGuid.ToString() + "," + + Containers::String(std::to_string(assetRef.localID).c_str()) + "," + + Containers::String(std::to_string(static_cast(assetRef.resourceType)).c_str()); +} + template 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 lock(m_ioMutex); m_assetDatabase.Shutdown(); ResourceFileSystem::Get().Shutdown(); + + std::lock_guard inFlightLock(m_inFlightLoadsMutex); + m_inFlightLoads.clear(); } void ResourceManager::SetResourceRoot(const Containers::String& rootPath) { + std::lock_guard 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); - - auto* it = m_resourceCache.Find(guid); - if (it != nullptr) { - IResource* resource = *it; - m_resourceCache.Erase(guid); - m_guidToPath.Erase(guid); - m_memoryUsage -= resource->GetMemorySize(); + IResource* resource = nullptr; + { + std::lock_guard lock(m_mutex); + + 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 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 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 ResourceManager::GetResourcePaths() const } void ResourceManager::UnloadGroup(const Containers::Array& 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 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& guids) void ResourceManager::RefreshAssetDatabase() { if (!m_resourceRoot.Empty()) { + std::lock_guard 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 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 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 inFlightState; + bool shouldExecuteLoad = false; + { + std::unique_lock inFlightLock(m_inFlightLoadsMutex); + auto inFlightIt = m_inFlightLoads.find(inFlightKey); + if (inFlightIt == m_inFlightLoads.end()) { + inFlightState = std::make_shared(); + m_inFlightLoads.emplace(inFlightKey, inFlightState); + shouldExecuteLoad = true; + } else { + inFlightState = inFlightIt->second; + } + } + + auto completeInFlightLoad = [&](const LoadResult& result) { + { + std::lock_guard 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 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 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; } diff --git a/engine/src/Scene/Scene.cpp b/engine/src/Scene/Scene.cpp index aa23bb81..149b682f 100644 --- a/engine/src/Scene/Scene.cpp +++ b/engine/src/Scene/Scene.cpp @@ -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 @@ -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()); } diff --git a/tests/Components/test_mesh_render_components.cpp b/tests/Components/test_mesh_render_components.cpp index 2c6cc360..de4e4663 100644 --- a/tests/Components/test_mesh_render_components.cpp +++ b/tests/Components/test_mesh_render_components.cpp @@ -5,12 +5,15 @@ #include #include #include +#include #include #include #include #include #include +#include +#include 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 GetSupportedExtensions() const override { + XCEngine::Containers::Array extensions; + extensions.PushBack("mesh"); + return extensions; + } + + bool CanLoad(const XCEngine::Containers::String& path) const override { + (void)path; + return true; + } + + LoadResult Load(const XCEngine::Containers::String& path, + const ImportSettings* settings = nullptr) override { + (void)settings; + + auto* mesh = new Mesh(); + IResource::ConstructParams params = {}; + params.name = "AsyncMesh"; + params.path = path; + params.guid = ResourceGUID::Generate(path); + mesh->Initialize(params); + + const StaticMeshVertex vertices[3] = {}; + const XCEngine::Core::uint32 indices[3] = {0, 1, 2}; + mesh->SetVertexData(vertices, sizeof(vertices), 3, sizeof(StaticMeshVertex), VertexAttribute::Position); + mesh->SetIndexData(indices, sizeof(indices), 3, true); + return LoadResult(mesh); + } + + ImportSettings* GetDefaultSettings() const override { + return nullptr; + } +}; + +bool PumpAsyncLoadsUntilIdle(ResourceManager& manager, + std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (manager.IsAsyncLoading() && std::chrono::steady_clock::now() < deadline) { + manager.UpdateAsyncLoads(); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + manager.UpdateAsyncLoads(); + return !manager.IsAsyncLoading(); +} + TEST(MeshFilterComponent_Test, SetMeshCachesResourceAndPath) { GameObject gameObject("MeshHolder"); auto* component = gameObject.AddComponent(); @@ -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(); @@ -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 diff --git a/tests/Core/Asset/CMakeLists.txt b/tests/Core/Asset/CMakeLists.txt index 209308ef..2e60afb7 100644 --- a/tests/Core/Asset/CMakeLists.txt +++ b/tests/Core/Asset/CMakeLists.txt @@ -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 ) diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp index e9deb80e..332811bd 100644 --- a/tests/Scene/test_scene.cpp +++ b/tests/Scene/test_scene.cpp @@ -10,10 +10,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #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 FindGameObjectsByMeshPath(Scene& scene, const std::string& meshPath) { + std::vector matches; + const std::vector meshFilters = scene.FindObjectsOfType(); + 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 backpackObjects = + FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj"); + ASSERT_EQ(backpackObjects.size(), 2u); - auto* meshFilter = backpackObject->GetComponent(); - auto* meshRenderer = backpackObject->GetComponent(); - 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(); + auto* meshRenderer = backpackObject->GetComponent(); + 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(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 backpackObjects = + FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj"); + ASSERT_EQ(backpackObjects.size(), 2u); + + std::vector backpackMeshFilters; + std::vector backpackMeshRenderers; + for (GameObject* backpackObject : backpackObjects) { + ASSERT_NE(backpackObject, nullptr); + auto* meshFilter = backpackObject->GetComponent(); + auto* meshRenderer = backpackObject->GetComponent(); + 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 backpackObjects = + FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj"); + ASSERT_EQ(backpackObjects.size(), 2u); + + std::vector backpackMeshFilters; + for (GameObject* backpackObject : backpackObjects) { + ASSERT_NE(backpackObject, nullptr); + auto* meshFilter = backpackObject->GetComponent(); + 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 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 diff --git a/tests/core/Asset/test_resource_manager.cpp b/tests/core/Asset/test_resource_manager.cpp new file mode 100644 index 00000000..b5e30e4d --- /dev/null +++ b/tests/core/Asset/test_resource_manager.cpp @@ -0,0 +1,158 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace XCEngine::Resources; + +namespace { + +class BlockingMeshLoader : public IResourceLoader { +public: + ResourceType GetResourceType() const override { return ResourceType::Mesh; } + + XCEngine::Containers::Array GetSupportedExtensions() const override { + XCEngine::Containers::Array extensions; + extensions.PushBack("mesh"); + return extensions; + } + + bool CanLoad(const XCEngine::Containers::String& path) const override { + (void)path; + return true; + } + + LoadResult Load(const XCEngine::Containers::String& path, + const ImportSettings* settings = nullptr) override { + (void)settings; + + ++m_loadCalls; + { + std::lock_guard lock(m_mutex); + m_started = true; + } + m_condition.notify_all(); + + { + std::unique_lock 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 lock(m_mutex); + return m_condition.wait_for(lock, timeout, [this]() { return m_started; }); + } + + void AllowCompletion() { + { + std::lock_guard lock(m_mutex); + m_allowCompletion = true; + } + m_condition.notify_all(); + } + + int GetLoadCalls() const { + return m_loadCalls.load(); + } + +private: + std::atomic 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& 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 callbackResources; + std::atomic callbackCount{0}; + + const auto callback = [&](LoadResult result) { + EXPECT_TRUE(result); + EXPECT_NE(result.resource, nullptr); + { + std::lock_guard 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