Add deferred async scene asset loading

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

View File

@@ -9,7 +9,9 @@
#include "UI/BuiltInIcons.h" #include "UI/BuiltInIcons.h"
#include "Platform/Win32Utf8.h" #include "Platform/Win32Utf8.h"
#include "Platform/WindowsProcessDiagnostics.h" #include "Platform/WindowsProcessDiagnostics.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Debug/Logger.h> #include <XCEngine/Debug/Logger.h>
#include <chrono>
#include <windows.h> #include <windows.h>
namespace XCEngine { namespace XCEngine {
@@ -141,12 +143,18 @@ bool Application::Initialize(HWND hwnd) {
return false; 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..."); logger.Info(Debug::LogCategory::General, "Initializing editor context...");
InitializeEditorContext(projectRoot); InitializeEditorContext(projectRoot);
logger.Info(Debug::LogCategory::General, "Initializing ImGui backend..."); logger.Info(Debug::LogCategory::General, "Initializing ImGui backend...");
InitializeImGui(hwnd); InitializeImGui(hwnd);
logger.Info(Debug::LogCategory::General, "Attaching editor layer..."); logger.Info(Debug::LogCategory::General, "Attaching editor layer...");
AttachEditorLayer(); AttachEditorLayer();
m_hasLastFrameTime = false;
logger.Info(Debug::LogCategory::General, "Editor initialization completed."); logger.Info(Debug::LogCategory::General, "Editor initialization completed.");
m_renderReady = true; m_renderReady = true;
return true; return true;
@@ -154,6 +162,7 @@ bool Application::Initialize(HWND hwnd) {
void Application::Shutdown() { void Application::Shutdown() {
m_renderReady = false; m_renderReady = false;
m_hasLastFrameTime = false;
DetachEditorLayer(); DetachEditorLayer();
if (m_editorContext) { if (m_editorContext) {
static_cast<EditorContext*>(m_editorContext.get())->SetViewportHostService(nullptr); static_cast<EditorContext*>(m_editorContext.get())->SetViewportHostService(nullptr);
@@ -163,6 +172,10 @@ void Application::Shutdown() {
m_imguiBackend.Shutdown(); m_imguiBackend.Shutdown();
m_imguiSession.Shutdown(); m_imguiSession.Shutdown();
ShutdownEditorContext(); ShutdownEditorContext();
if (m_resourceManagerInitialized) {
::XCEngine::Resources::ResourceManager::Get().Shutdown();
m_resourceManagerInitialized = false;
}
m_windowRenderer.Shutdown(); m_windowRenderer.Shutdown();
} }
@@ -170,6 +183,16 @@ void Application::Render() {
if (!m_renderReady) { if (!m_renderReady) {
return; 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(); RenderEditorFrame();
} }
@@ -206,6 +229,8 @@ bool Application::SwitchProject(const std::string& projectPath) {
const std::string infoMessage = "Switched editor project root: " + projectPath; const std::string infoMessage = "Switched editor project root: " + projectPath;
logger.Info(Debug::LogCategory::General, infoMessage.c_str()); logger.Info(Debug::LogCategory::General, infoMessage.c_str());
::XCEngine::Resources::ResourceManager::Get().SetResourceRoot(projectPath.c_str());
m_lastWindowTitle.clear(); m_lastWindowTitle.clear();
UpdateWindowTitle(); UpdateWindowTitle();

View File

@@ -6,6 +6,7 @@
#include "Viewport/ViewportHostService.h" #include "Viewport/ViewportHostService.h"
#include <XCEngine/Rendering/RenderContext.h> #include <XCEngine/Rendering/RenderContext.h>
#include <chrono>
#include <memory> #include <memory>
#include <string> #include <string>
#include <windows.h> #include <windows.h>
@@ -65,7 +66,10 @@ private:
ViewportHostService m_viewportHostService; ViewportHostService m_viewportHostService;
uint64_t m_exitRequestedHandlerId = 0; uint64_t m_exitRequestedHandlerId = 0;
std::wstring m_lastWindowTitle; std::wstring m_lastWindowTitle;
std::chrono::steady_clock::time_point m_lastFrameTime{};
bool m_hasLastFrameTime = false;
bool m_renderReady = false; bool m_renderReady = false;
bool m_resourceManagerInitialized = false;
}; };
} }

View File

@@ -12,6 +12,8 @@
#include "panels/ProjectPanel.h" #include "panels/ProjectPanel.h"
#include "panels/SceneViewPanel.h" #include "panels/SceneViewPanel.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -53,6 +55,7 @@ public:
} }
void Update(float dt) { void Update(float dt) {
::XCEngine::Resources::ResourceManager::Get().UpdateAsyncLoads();
m_panels.UpdateAll(dt); m_panels.UpdateAll(dt);
} }

View File

@@ -2,6 +2,7 @@
#include "Core/EventBus.h" #include "Core/EventBus.h"
#include "Core/EditorEvents.h" #include "Core/EditorEvents.h"
#include "Utils/ProjectFileUtils.h" #include "Utils/ProjectFileUtils.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Components/ComponentFactoryRegistry.h> #include <XCEngine/Components/ComponentFactoryRegistry.h>
#include <XCEngine/Components/CameraComponent.h> #include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h> #include <XCEngine/Components/LightComponent.h>
@@ -192,7 +193,10 @@ bool SceneManager::RestoreSceneSnapshot(const SceneSnapshot& snapshot) {
} }
auto scene = std::make_unique<::XCEngine::Components::Scene>(); auto scene = std::make_unique<::XCEngine::Components::Scene>();
{
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
scene->DeserializeFromString(snapshot.sceneData); scene->DeserializeFromString(snapshot.sceneData);
}
m_scene = std::move(scene); m_scene = std::move(scene);
m_clipboard.reset(); m_clipboard.reset();
@@ -245,7 +249,10 @@ bool SceneManager::LoadScene(const std::string& filePath) {
} }
auto scene = std::make_unique<::XCEngine::Components::Scene>(); auto scene = std::make_unique<::XCEngine::Components::Scene>();
{
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
scene->Load(filePath); scene->Load(filePath);
}
m_scene = std::move(scene); m_scene = std::move(scene);
m_clipboard.reset(); m_clipboard.reset();

View File

@@ -5,6 +5,7 @@
#include <XCEngine/Core/Asset/ResourceHandle.h> #include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Resources/Mesh/Mesh.h> #include <XCEngine/Resources/Mesh/Mesh.h>
#include <memory>
#include <string> #include <string>
namespace XCEngine { namespace XCEngine {
@@ -14,8 +15,8 @@ class MeshFilterComponent : public Component {
public: public:
std::string GetName() const override { return "MeshFilter"; } std::string GetName() const override { return "MeshFilter"; }
Resources::Mesh* GetMesh() const { return m_mesh.Get(); } Resources::Mesh* GetMesh() const;
const Resources::ResourceHandle<Resources::Mesh>& GetMeshHandle() const { return m_mesh; } const Resources::ResourceHandle<Resources::Mesh>& GetMeshHandle() const;
const std::string& GetMeshPath() const { return m_meshPath; } const std::string& GetMeshPath() const { return m_meshPath; }
const Resources::AssetRef& GetMeshAssetRef() const { return m_meshRef; } const Resources::AssetRef& GetMeshAssetRef() const { return m_meshRef; }
@@ -28,9 +29,15 @@ public:
void Deserialize(std::istream& is) override; void Deserialize(std::istream& is) override;
private: private:
struct PendingMeshLoadState;
void BeginAsyncMeshLoad(const std::string& meshPath);
void ResolvePendingMesh();
Resources::ResourceHandle<Resources::Mesh> m_mesh; Resources::ResourceHandle<Resources::Mesh> m_mesh;
std::string m_meshPath; std::string m_meshPath;
Resources::AssetRef m_meshRef; Resources::AssetRef m_meshRef;
std::shared_ptr<PendingMeshLoadState> m_pendingMeshLoad;
}; };
} // namespace Components } // namespace Components

View File

@@ -6,6 +6,7 @@
#include <XCEngine/Resources/Material/Material.h> #include <XCEngine/Resources/Material/Material.h>
#include <cstdint> #include <cstdint>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -42,12 +43,17 @@ public:
void Deserialize(std::istream& is) override; void Deserialize(std::istream& is) override;
private: private:
struct PendingMaterialLoadState;
void BeginAsyncMaterialLoad(size_t index, const std::string& materialPath);
void ResolvePendingMaterials();
void EnsureMaterialSlot(size_t index); void EnsureMaterialSlot(size_t index);
static std::string MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material); static std::string MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material);
std::vector<Resources::ResourceHandle<Resources::Material>> m_materials; std::vector<Resources::ResourceHandle<Resources::Material>> m_materials;
std::vector<std::string> m_materialPaths; std::vector<std::string> m_materialPaths;
std::vector<Resources::AssetRef> m_materialRefs; std::vector<Resources::AssetRef> m_materialRefs;
std::vector<std::shared_ptr<PendingMaterialLoadState>> m_pendingMaterialLoads;
bool m_castShadows = true; bool m_castShadows = true;
bool m_receiveShadows = true; bool m_receiveShadows = true;
uint32_t m_renderLayer = 0; uint32_t m_renderLayer = 0;

View File

@@ -2,10 +2,13 @@
#include <XCEngine/Core/IO/IResourceLoader.h> #include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Core/Asset/ImportSettings.h> #include <XCEngine/Core/Asset/ImportSettings.h>
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Threading/Mutex.h>
#include <atomic> #include <atomic>
#include <condition_variable>
#include <deque>
#include <functional> #include <functional>
#include <mutex>
#include <thread>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Resources { namespace Resources {
@@ -50,6 +53,14 @@ private:
static Core::uint64 GenerateRequestId(); 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 { class AsyncLoader {
public: public:
static AsyncLoader& Get(); static AsyncLoader& Get();
@@ -76,18 +87,21 @@ public:
private: private:
void SubmitInternal(LoadRequest request); void SubmitInternal(LoadRequest request);
IResourceLoader* FindLoader(ResourceType type) const; IResourceLoader* FindLoader(ResourceType type) const;
void WorkerThread();
void QueueCompleted(LoadRequest request, LoadResult result); void QueueCompleted(LoadRequest request, LoadResult result);
Threading::Mutex m_queueMutex; std::mutex m_queueMutex;
Containers::Array<LoadRequest> m_pendingQueue; std::condition_variable m_pendingCondition;
std::deque<LoadRequest> m_pendingQueue;
Threading::Mutex m_completedMutex; std::mutex m_completedMutex;
Containers::Array<LoadRequest> m_completedQueue; 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_pendingCount{0};
std::atomic<Core::uint32> m_completedCount{0}; std::atomic<Core::uint32> m_completedCount{0};
Core::uint32 m_totalRequested = 0; std::atomic<Core::uint64> m_totalRequested{0};
}; };
} // namespace Resources } // namespace Resources

View File

@@ -11,6 +11,11 @@
#include <XCEngine/Core/Containers/HashMap.h> #include <XCEngine/Core/Containers/HashMap.h>
#include <XCEngine/Threading/Mutex.h> #include <XCEngine/Threading/Mutex.h>
#include <XCEngine/Debug/Logger.h> #include <XCEngine/Debug/Logger.h>
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <type_traits> #include <type_traits>
namespace XCEngine { namespace XCEngine {
@@ -18,6 +23,17 @@ namespace Resources {
class ResourceManager { class ResourceManager {
public: 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(); static ResourceManager& Get();
void Initialize(); void Initialize();
@@ -43,7 +59,7 @@ public:
static_assert(std::is_base_of_v<IResource, T>, "T must derive from IResource"); static_assert(std::is_base_of_v<IResource, T>, "T must derive from IResource");
Containers::String path; Containers::String path;
if (!assetRef.IsValid() || !m_assetDatabase.TryGetPrimaryAssetPath(assetRef.assetGuid, path)) { if (!TryResolveAssetPath(assetRef, path)) {
return ResourceHandle<T>(); return ResourceHandle<T>();
} }
@@ -54,6 +70,9 @@ public:
std::function<void(LoadResult)> callback); std::function<void(LoadResult)> callback);
void LoadAsync(const Containers::String& path, ResourceType type, ImportSettings* settings, void LoadAsync(const Containers::String& path, ResourceType type, ImportSettings* settings,
std::function<void(LoadResult)> callback); std::function<void(LoadResult)> callback);
void UpdateAsyncLoads();
bool IsAsyncLoading() const;
Core::uint32 GetAsyncPendingCount() const;
void Unload(const Containers::String& path); void Unload(const Containers::String& path);
void Unload(ResourceGUID guid); void Unload(ResourceGUID guid);
@@ -98,8 +117,35 @@ public:
void UnloadGroup(const Containers::Array<ResourceGUID>& guids); void UnloadGroup(const Containers::Array<ResourceGUID>& guids);
void RefreshAssetDatabase(); void RefreshAssetDatabase();
bool TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const; 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: 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;
~ResourceManager() = default; ~ResourceManager() = default;
@@ -122,8 +168,13 @@ private:
ResourceCache m_cache; ResourceCache m_cache;
Core::UniqueRef<AsyncLoader> m_asyncLoader; Core::UniqueRef<AsyncLoader> m_asyncLoader;
Threading::Mutex m_mutex; 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 ResourceHandleBase;
friend class AsyncLoader;
}; };
} // namespace Resources } // namespace Resources

View File

@@ -1,6 +1,8 @@
#include "Components/MeshFilterComponent.h" #include "Components/MeshFilterComponent.h"
#include "Components/GameObject.h"
#include "Core/Asset/ResourceManager.h" #include "Core/Asset/ResourceManager.h"
#include "Debug/Logger.h"
#include <sstream> #include <sstream>
@@ -13,6 +15,11 @@ std::string ToStdString(const Containers::String& value) {
return std::string(value.CStr()); 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) { std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
if (!assetRef.IsValid()) { if (!assetRef.IsValid()) {
return std::string(); return std::string();
@@ -37,9 +44,40 @@ bool TryDecodeAssetRef(const std::string& value, Resources::AssetRef& outRef) {
return outRef.IsValid(); 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 } // 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) { void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
m_pendingMeshLoad.reset();
m_meshPath = meshPath; m_meshPath = meshPath;
if (m_meshPath.empty()) { if (m_meshPath.empty()) {
m_mesh.Reset(); 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)) { if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
m_meshRef.Reset(); 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) { void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) {
m_pendingMeshLoad.reset();
m_mesh = mesh; m_mesh = mesh;
m_meshPath = mesh.Get() != nullptr ? ToStdString(mesh->GetPath()) : std::string(); m_meshPath = mesh.Get() != nullptr ? ToStdString(mesh->GetPath()) : std::string();
if (m_meshPath.empty()) { if (m_meshPath.empty()) {
@@ -68,6 +115,7 @@ void MeshFilterComponent::SetMesh(Resources::Mesh* mesh) {
} }
void MeshFilterComponent::ClearMesh() { void MeshFilterComponent::ClearMesh() {
m_pendingMeshLoad.reset();
m_mesh.Reset(); m_mesh.Reset();
m_meshPath.clear(); m_meshPath.clear();
m_meshRef.Reset(); m_meshRef.Reset();
@@ -79,6 +127,7 @@ void MeshFilterComponent::Serialize(std::ostream& os) const {
} }
void MeshFilterComponent::Deserialize(std::istream& is) { void MeshFilterComponent::Deserialize(std::istream& is) {
m_pendingMeshLoad.reset();
m_mesh.Reset(); m_mesh.Reset();
m_meshPath.clear(); m_meshPath.clear();
m_meshRef.Reset(); 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()) { if (pendingMeshRef.IsValid()) {
m_meshRef = pendingMeshRef; 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); m_mesh = Resources::ResourceManager::Get().Load<Resources::Mesh>(pendingMeshRef);
if (m_mesh.Get() != nullptr) { if (m_mesh.Get() != nullptr) {
m_meshPath = ToStdString(m_mesh->GetPath()); m_meshPath = ToStdString(m_mesh->GetPath());
@@ -115,9 +186,79 @@ void MeshFilterComponent::Deserialize(std::istream& is) {
m_meshPath = pendingMeshPath; m_meshPath = pendingMeshPath;
} }
} else if (!pendingMeshPath.empty()) { } 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); 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 Components
} // namespace XCEngine } // namespace XCEngine

View File

@@ -1,6 +1,8 @@
#include "Components/MeshRendererComponent.h" #include "Components/MeshRendererComponent.h"
#include "Components/GameObject.h"
#include "Core/Asset/ResourceManager.h" #include "Core/Asset/ResourceManager.h"
#include "Debug/Logger.h"
#include <sstream> #include <sstream>
@@ -13,6 +15,12 @@ std::string ToStdString(const Containers::String& value) {
return std::string(value.CStr()); 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) { std::string EncodeAssetRef(const Resources::AssetRef& assetRef) {
if (!assetRef.IsValid()) { if (!assetRef.IsValid()) {
return std::string(); return std::string();
@@ -83,13 +91,66 @@ std::vector<Resources::AssetRef> SplitMaterialRefs(const std::string& value) {
return refs; 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 } // namespace
struct MeshRendererComponent::PendingMaterialLoadState {
Resources::LoadResult result;
bool completed = false;
};
Resources::Material* MeshRendererComponent::GetMaterial(size_t index) const { Resources::Material* MeshRendererComponent::GetMaterial(size_t index) const {
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
return index < m_materials.size() ? m_materials[index].Get() : nullptr; return index < m_materials.size() ? m_materials[index].Get() : nullptr;
} }
const Resources::ResourceHandle<Resources::Material>& MeshRendererComponent::GetMaterialHandle(size_t index) const { const Resources::ResourceHandle<Resources::Material>& MeshRendererComponent::GetMaterialHandle(size_t index) const {
const_cast<MeshRendererComponent*>(this)->ResolvePendingMaterials();
static const Resources::ResourceHandle<Resources::Material> kNullHandle; static const Resources::ResourceHandle<Resources::Material> kNullHandle;
return index < m_materials.size() ? m_materials[index] : 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) { void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& materialPath) {
EnsureMaterialSlot(index); EnsureMaterialSlot(index);
m_pendingMaterialLoads[index].reset();
m_materialPaths[index] = materialPath; m_materialPaths[index] = materialPath;
if (materialPath.empty()) { if (materialPath.empty()) {
m_materials[index].Reset(); 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])) { if (!Resources::ResourceManager::Get().TryGetAssetRef(materialPath.c_str(), Resources::ResourceType::Material, m_materialRefs[index])) {
m_materialRefs[index].Reset(); 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) { void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material) {
EnsureMaterialSlot(index); EnsureMaterialSlot(index);
m_pendingMaterialLoads[index].reset();
m_materials[index] = material; m_materials[index] = material;
m_materialPaths[index] = MaterialPathFromHandle(material); m_materialPaths[index] = MaterialPathFromHandle(material);
if (m_materialPaths[index].empty() || if (m_materialPaths[index].empty() ||
@@ -132,6 +204,8 @@ void MeshRendererComponent::SetMaterials(const std::vector<Resources::ResourceHa
m_materials = materials; m_materials = materials;
m_materialPaths.resize(materials.size()); m_materialPaths.resize(materials.size());
m_materialRefs.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) { for (size_t i = 0; i < materials.size(); ++i) {
m_materialPaths[i] = MaterialPathFromHandle(materials[i]); m_materialPaths[i] = MaterialPathFromHandle(materials[i]);
if (m_materialPaths[i].empty() || if (m_materialPaths[i].empty() ||
@@ -145,6 +219,7 @@ void MeshRendererComponent::ClearMaterials() {
m_materials.clear(); m_materials.clear();
m_materialPaths.clear(); m_materialPaths.clear();
m_materialRefs.clear(); m_materialRefs.clear();
m_pendingMaterialLoads.clear();
} }
void MeshRendererComponent::Serialize(std::ostream& os) const { void MeshRendererComponent::Serialize(std::ostream& os) const {
@@ -194,6 +269,7 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
m_materialPaths = SplitMaterialPaths(value); m_materialPaths = SplitMaterialPaths(value);
m_materials.resize(m_materialPaths.size()); m_materials.resize(m_materialPaths.size());
m_materialRefs.resize(m_materialPaths.size()); m_materialRefs.resize(m_materialPaths.size());
m_pendingMaterialLoads.resize(m_materialPaths.size());
} else if (key == "materialRefs") { } else if (key == "materialRefs") {
pendingMaterialRefs = SplitMaterialRefs(value); pendingMaterialRefs = SplitMaterialRefs(value);
} else if (key == "castShadows") { } else if (key == "castShadows") {
@@ -209,28 +285,149 @@ void MeshRendererComponent::Deserialize(std::istream& is) {
m_materialPaths.resize(pendingMaterialRefs.size()); m_materialPaths.resize(pendingMaterialRefs.size());
m_materials.resize(pendingMaterialRefs.size()); m_materials.resize(pendingMaterialRefs.size());
m_materialRefs.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) { for (size_t i = 0; i < m_materialPaths.size(); ++i) {
bool restoredOrQueued = false;
if (i < pendingMaterialRefs.size() && pendingMaterialRefs[i].IsValid()) { if (i < pendingMaterialRefs.size() && pendingMaterialRefs[i].IsValid()) {
m_materialRefs[i] = pendingMaterialRefs[i]; m_materialRefs[i] = 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]); m_materials[i] = Resources::ResourceManager::Get().Load<Resources::Material>(pendingMaterialRefs[i]);
}
if (m_materials[i].Get() != nullptr) { if (m_materials[i].Get() != nullptr) {
m_materialPaths[i] = MaterialPathFromHandle(m_materials[i]); m_materialPaths[i] = MaterialPathFromHandle(m_materials[i]);
restoredOrQueued = true;
}
if (restoredOrQueued) {
continue; 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]); 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) { void MeshRendererComponent::EnsureMaterialSlot(size_t index) {
if (index >= m_materials.size()) { if (index >= m_materials.size()) {
m_materials.resize(index + 1); m_materials.resize(index + 1);
}
if (index >= m_materialPaths.size()) {
m_materialPaths.resize(index + 1); m_materialPaths.resize(index + 1);
}
if (index >= m_materialRefs.size()) {
m_materialRefs.resize(index + 1); 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) { std::string MeshRendererComponent::MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Material>& material) {

View File

@@ -1,6 +1,7 @@
#include <XCEngine/Core/Asset/AssetDatabase.h> #include <XCEngine/Core/Asset/AssetDatabase.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h> #include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Resources/Mesh/MeshLoader.h> #include <XCEngine/Resources/Mesh/MeshLoader.h>
#include <XCEngine/Resources/Texture/TextureLoader.h> #include <XCEngine/Resources/Texture/TextureLoader.h>
@@ -23,6 +24,17 @@ std::string ToStdString(const Containers::String& value) {
return std::string(value.CStr()); 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) { Containers::String ToContainersString(const std::string& value) {
return Containers::String(value.c_str()); return Containers::String(value.c_str());
} }
@@ -368,6 +380,10 @@ bool AssetDatabase::ResolvePath(const Containers::String& requestPath,
return false; return false;
} }
if (HasVirtualPathScheme(requestPath)) {
return false;
}
fs::path inputPath(requestPath.CStr()); fs::path inputPath(requestPath.CStr());
if (inputPath.is_absolute()) { if (inputPath.is_absolute()) {
outAbsolutePath = NormalizePathString(inputPath); outAbsolutePath = NormalizePathString(inputPath);
@@ -863,11 +879,24 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
Containers::String absolutePath; Containers::String absolutePath;
Containers::String relativePath; Containers::String relativePath;
if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) { 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; return false;
} }
const fs::path absoluteFsPath(absolutePath.CStr()); const fs::path absoluteFsPath(absolutePath.CStr());
if (!fs::exists(absoluteFsPath)) { 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; return false;
} }
@@ -876,8 +905,31 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
return false; 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); const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
if (primaryType == ResourceType::Unknown || primaryType != requestedType) { 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; return false;
} }
@@ -888,8 +940,18 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
} }
if (ShouldReimport(sourceRecord, artifactRecord)) { if (ShouldReimport(sourceRecord, artifactRecord)) {
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact reimport path=") + requestPath);
}
ArtifactRecord rebuiltRecord; ArtifactRecord rebuiltRecord;
if (!ImportAsset(sourceRecord, 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; 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.artifactDirectory = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->artifactDirectory.CStr());
outAsset.artifactMainPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr()); outAsset.artifactMainPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr());
outAsset.mainLocalID = artifactRecord->mainLocalID; 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; return true;
} }

View File

@@ -1,10 +1,30 @@
#include <XCEngine/Core/Asset/AsyncLoader.h> #include <XCEngine/Core/Asset/AsyncLoader.h>
#include <XCEngine/Core/Asset/ResourceManager.h> #include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Asset/ResourceTypes.h> #include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Debug/Logger.h>
namespace XCEngine { namespace XCEngine {
namespace Resources { 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() { Core::uint64 LoadRequest::GenerateRequestId() {
static std::atomic<Core::uint64> s_requestId{0}; static std::atomic<Core::uint64> s_requestId{0};
return ++s_requestId; return ++s_requestId;
@@ -16,11 +36,43 @@ AsyncLoader& AsyncLoader::Get() {
} }
void AsyncLoader::Initialize(Core::uint32 workerThreadCount) { 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() { 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, void AsyncLoader::Submit(const Containers::String& path, ResourceType type,
@@ -47,56 +99,185 @@ void AsyncLoader::SubmitInternal(LoadRequest request) {
} }
{ {
std::lock_guard lock(m_queueMutex); std::lock_guard<std::mutex> lock(m_queueMutex);
m_pendingQueue.PushBack(std::move(request)); if (!m_running) {
m_pendingCount++; if (request.callback) {
m_totalRequested++; 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() { void AsyncLoader::Update() {
Containers::Array<LoadRequest> completed; std::deque<CompletedLoadRequest> completed;
{ {
std::lock_guard lock(m_completedMutex); std::lock_guard<std::mutex> lock(m_completedMutex);
completed = std::move(m_completedQueue); completed.swap(m_completedQueue);
m_completedQueue.Clear();
} }
for (auto& request : completed) { for (CompletedLoadRequest& entry : completed) {
m_pendingCount--; if (m_pendingCount > 0) {
--m_pendingCount;
}
++m_completedCount;
if (request.callback) { if (ShouldTraceAsyncPath(entry.request.path)) {
LoadResult result(true); TraceAsyncLoad(
request.callback(result); 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 { float AsyncLoader::GetProgress() const {
if (m_totalRequested == 0) return 1.0f; const Core::uint64 totalRequested = m_totalRequested.load();
return static_cast<float>(m_totalRequested - m_pendingCount.load()) / m_totalRequested; if (totalRequested == 0) {
return 1.0f;
}
return static_cast<float>(totalRequested - m_pendingCount.load()) /
static_cast<float>(totalRequested);
} }
void AsyncLoader::CancelAll() { void AsyncLoader::CancelAll() {
std::lock_guard lock(m_queueMutex); std::lock_guard<std::mutex> lock(m_queueMutex);
m_pendingQueue.Clear(); 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; m_pendingCount = 0;
} }
}
}
void AsyncLoader::Cancel(Core::uint64 requestId) { void AsyncLoader::Cancel(Core::uint64 requestId) {
std::lock_guard lock(m_queueMutex); std::lock_guard<std::mutex> lock(m_queueMutex);
(void)requestId; 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 { IResourceLoader* AsyncLoader::FindLoader(ResourceType type) const {
return ResourceManager::Get().GetLoader(type); 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) { void AsyncLoader::QueueCompleted(LoadRequest request, LoadResult result) {
std::lock_guard lock(m_completedMutex); std::lock_guard<std::mutex> lock(m_completedMutex);
(void)request; m_completedQueue.emplace_back(std::move(request), std::move(result));
(void)result;
} }
} // namespace Resources } // namespace Resources

View File

@@ -6,12 +6,34 @@
#include <XCEngine/Resources/Mesh/MeshLoader.h> #include <XCEngine/Resources/Mesh/MeshLoader.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h> #include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <XCEngine/Resources/Texture/TextureLoader.h> #include <XCEngine/Resources/Texture/TextureLoader.h>
#include <exception>
namespace XCEngine { namespace XCEngine {
namespace Resources { namespace Resources {
namespace { 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> template<typename TLoader>
void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) { void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) {
if (manager.GetLoader(loader.GetResourceType()) == nullptr) { if (manager.GetLoader(loader.GetResourceType()) == nullptr) {
@@ -31,6 +53,17 @@ ResourceManager& ResourceManager::Get() {
return instance; 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() { void ResourceManager::Initialize() {
if (m_asyncLoader) { if (m_asyncLoader) {
return; return;
@@ -51,11 +84,17 @@ void ResourceManager::Shutdown() {
m_asyncLoader->Shutdown(); m_asyncLoader->Shutdown();
m_asyncLoader.reset(); m_asyncLoader.reset();
} }
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
m_assetDatabase.Shutdown(); m_assetDatabase.Shutdown();
ResourceFileSystem::Get().Shutdown(); ResourceFileSystem::Get().Shutdown();
std::lock_guard<std::mutex> inFlightLock(m_inFlightLoadsMutex);
m_inFlightLoads.clear();
} }
void ResourceManager::SetResourceRoot(const Containers::String& rootPath) { void ResourceManager::SetResourceRoot(const Containers::String& rootPath) {
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
m_resourceRoot = rootPath; m_resourceRoot = rootPath;
if (!m_resourceRoot.Empty()) { if (!m_resourceRoot.Empty()) {
ResourceFileSystem::Get().Initialize(rootPath); ResourceFileSystem::Get().Initialize(rootPath);
@@ -139,33 +178,50 @@ void ResourceManager::AddToCache(ResourceGUID guid, IResource* resource) {
} }
void ResourceManager::Unload(ResourceGUID guid) { void ResourceManager::Unload(ResourceGUID guid) {
IResource* resource = nullptr;
{
std::lock_guard lock(m_mutex); std::lock_guard lock(m_mutex);
auto* it = m_resourceCache.Find(guid); auto* it = m_resourceCache.Find(guid);
if (it != nullptr) { if (it != nullptr) {
IResource* resource = *it; resource = *it;
m_resourceCache.Erase(guid); m_resourceCache.Erase(guid);
m_guidToPath.Erase(guid); m_guidToPath.Erase(guid);
m_memoryUsage -= resource->GetMemorySize(); m_memoryUsage -= resource->GetMemorySize();
}
}
if (resource != nullptr) {
resource->Release(); resource->Release();
} }
} }
void ResourceManager::UnloadAll() { void ResourceManager::UnloadAll() {
Containers::Array<IResource*> resourcesToRelease;
{
std::lock_guard lock(m_mutex); std::lock_guard lock(m_mutex);
const auto cachedResources = m_resourceCache.GetPairs(); const auto cachedResources = m_resourceCache.GetPairs();
resourcesToRelease.Reserve(cachedResources.Size());
for (const auto& pair : cachedResources) { for (const auto& pair : cachedResources) {
if (pair.second != nullptr) { if (pair.second != nullptr) {
pair.second->Release(); resourcesToRelease.PushBack(pair.second);
} }
} }
m_resourceCache.Clear(); m_resourceCache.Clear();
m_refCounts.Clear(); m_refCounts.Clear();
m_guidToPath.Clear(); m_guidToPath.Clear();
m_memoryUsage = 0; m_memoryUsage = 0;
} }
for (IResource* resource : resourcesToRelease) {
if (resource != nullptr) {
resource->Release();
}
}
}
void ResourceManager::SetMemoryBudget(size_t bytes) { void ResourceManager::SetMemoryBudget(size_t bytes) {
m_memoryBudget = bytes; m_memoryBudget = bytes;
} }
@@ -210,7 +266,28 @@ void ResourceManager::LoadAsync(const Containers::String& path, ResourceType typ
void ResourceManager::LoadAsync(const Containers::String& path, ResourceType type, void ResourceManager::LoadAsync(const Containers::String& path, ResourceType type,
ImportSettings* settings, ImportSettings* settings,
std::function<void(LoadResult)> callback) { 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) { void ResourceManager::Unload(const Containers::String& path) {
@@ -245,7 +322,10 @@ Containers::Array<Containers::String> ResourceManager::GetResourcePaths() const
} }
void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids) { void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids) {
Containers::Array<IResource*> resourcesToRelease;
{
std::lock_guard lock(m_mutex); std::lock_guard lock(m_mutex);
resourcesToRelease.Reserve(guids.Size());
for (const auto& guid : guids) { for (const auto& guid : guids) {
auto* it = m_resourceCache.Find(guid); auto* it = m_resourceCache.Find(guid);
if (it != nullptr) { if (it != nullptr) {
@@ -253,6 +333,13 @@ void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids)
m_resourceCache.Erase(guid); m_resourceCache.Erase(guid);
m_guidToPath.Erase(guid); m_guidToPath.Erase(guid);
m_memoryUsage -= resource->GetMemorySize(); m_memoryUsage -= resource->GetMemorySize();
resourcesToRelease.PushBack(resource);
}
}
}
for (IResource* resource : resourcesToRelease) {
if (resource != nullptr) {
resource->Release(); resource->Release();
} }
} }
@@ -260,12 +347,62 @@ void ResourceManager::UnloadGroup(const Containers::Array<ResourceGUID>& guids)
void ResourceManager::RefreshAssetDatabase() { void ResourceManager::RefreshAssetDatabase() {
if (!m_resourceRoot.Empty()) { if (!m_resourceRoot.Empty()) {
std::lock_guard<std::recursive_mutex> lock(m_ioMutex);
m_assetDatabase.Refresh(); m_assetDatabase.Refresh();
} }
} }
bool ResourceManager::TryGetAssetRef(const Containers::String& path, ResourceType resourceType, AssetRef& outRef) const { 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, LoadResult ResourceManager::LoadResource(const Containers::String& path,
@@ -273,7 +410,23 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
ImportSettings* settings) { ImportSettings* settings) {
const ResourceGUID guid = ResourceGUID::Generate(path); 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 (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); return LoadResult(cached);
} }
@@ -285,24 +438,125 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
return LoadResult(false, "Loader not found"); return LoadResult(false, "Loader not found");
} }
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; Containers::String loadPath = path;
{
std::lock_guard<std::recursive_mutex> ioLock(m_ioMutex);
AssetDatabase::ResolvedAsset resolvedAsset; AssetDatabase::ResolvedAsset resolvedAsset;
if (!m_resourceRoot.Empty() && if (!m_resourceRoot.Empty() &&
m_assetDatabase.EnsureArtifact(path, type, resolvedAsset) && m_assetDatabase.EnsureArtifact(path, type, resolvedAsset) &&
resolvedAsset.artifactReady) { resolvedAsset.artifactReady) {
loadPath = resolvedAsset.artifactMainPath; 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) { if (!result || result.resource == nullptr) {
Debug::Logger::Get().Error(Debug::LogCategory::FileSystem, Debug::Logger::Get().Error(Debug::LogCategory::FileSystem,
Containers::String("Failed to load resource: ") + path + " - " + result.errorMessage); Containers::String("Failed to load resource: ") + path + " - " + result.errorMessage);
completeInFlightLoad(result);
return 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; result.resource->m_path = path;
AddToCache(guid, result.resource); AddToCache(guid, result.resource);
{
std::lock_guard lock(m_mutex);
m_guidToPath.Insert(guid, path); m_guidToPath.Insert(guid, path);
}
completeInFlightLoad(result);
return result; return result;
} }

View File

@@ -1,5 +1,6 @@
#include "Scene/Scene.h" #include "Scene/Scene.h"
#include "Components/ComponentFactoryRegistry.h" #include "Components/ComponentFactoryRegistry.h"
#include "Debug/Logger.h"
#include "Components/GameObject.h" #include "Components/GameObject.h"
#include "Components/TransformComponent.h" #include "Components/TransformComponent.h"
#include <sstream> #include <sstream>
@@ -11,6 +12,16 @@ namespace Components {
namespace { 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 { struct PendingComponentData {
std::string type; std::string type;
std::string payload; std::string payload;
@@ -338,6 +349,19 @@ void Scene::DeserializeFromString(const std::string& data) {
} }
for (const PendingComponentData& componentData : pending.components) { 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 (Component* component = ComponentFactoryRegistry::Get().CreateComponent(go.get(), componentData.type)) {
if (!componentData.payload.empty()) { if (!componentData.payload.empty()) {
std::istringstream componentStream(componentData.payload); std::istringstream componentStream(componentData.payload);
@@ -396,11 +420,24 @@ std::string Scene::SerializeToString() const {
void Scene::Load(const std::string& filePath) { void Scene::Load(const std::string& filePath) {
std::ifstream file(filePath); std::ifstream file(filePath);
if (!file.is_open()) { 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; return;
} }
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[Scene] Load file=") + Containers::String(filePath.c_str()));
std::stringstream buffer; std::stringstream buffer;
buffer << file.rdbuf(); 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()); DeserializeFromString(buffer.str());
} }

View File

@@ -5,12 +5,15 @@
#include <XCEngine/Components/MeshRendererComponent.h> #include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Core/Asset/IResource.h> #include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceManager.h> #include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Resources/Material/Material.h> #include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Mesh/Mesh.h> #include <XCEngine/Resources/Mesh/Mesh.h>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <chrono>
#include <thread>
using namespace XCEngine::Components; using namespace XCEngine::Components;
using namespace XCEngine::Resources; using namespace XCEngine::Resources;
@@ -37,6 +40,56 @@ Material* CreateTestMaterial(const char* name, const char* path) {
return material; 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) { TEST(MeshFilterComponent_Test, SetMeshCachesResourceAndPath) {
GameObject gameObject("MeshHolder"); GameObject gameObject("MeshHolder");
auto* component = gameObject.AddComponent<MeshFilterComponent>(); auto* component = gameObject.AddComponent<MeshFilterComponent>();
@@ -82,6 +135,33 @@ TEST(MeshFilterComponent_Test, SetMeshPathPreservesPathWithoutLoadedResource) {
EXPECT_EQ(component.GetMesh(), nullptr); 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) { TEST(MeshRendererComponent_Test, SetMaterialsKeepsSlotsAndFlags) {
GameObject gameObject("RendererHolder"); GameObject gameObject("RendererHolder");
auto* component = gameObject.AddComponent<MeshRendererComponent>(); auto* component = gameObject.AddComponent<MeshRendererComponent>();
@@ -237,4 +317,58 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAs
fs::remove_all(projectRoot); 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 } // namespace

View File

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

View File

@@ -10,10 +10,13 @@
#include <XCEngine/Components/TransformComponent.h> #include <XCEngine/Components/TransformComponent.h>
#include <XCEngine/Core/Asset/ResourceManager.h> #include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Rendering/RenderSceneExtractor.h>
#include <XCEngine/Resources/Mesh/MeshLoader.h> #include <XCEngine/Resources/Mesh/MeshLoader.h>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <chrono>
#include <thread>
#ifdef _WIN32 #ifdef _WIN32
#ifndef NOMINMAX #ifndef NOMINMAX
@@ -31,6 +34,37 @@ std::filesystem::path GetRepositoryRoot() {
return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); 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 { class TestComponent : public Component {
public: public:
TestComponent() = default; 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 assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
const fs::path backpackMeshPath = projectRoot / "Assets" / "Models" / "backpack" / "backpack.obj"; 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(assimpDllPath));
ASSERT_TRUE(fs::exists(backpackMeshPath)); ASSERT_TRUE(fs::exists(backpackMeshPath));
@@ -602,6 +638,7 @@ TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) {
#endif #endif
fs::current_path(projectRoot); fs::current_path(projectRoot);
resourceManager.SetResourceRoot(projectRoot.string().c_str());
ASSERT_NE(resourceManager.GetLoader(XCEngine::Resources::ResourceType::Mesh), nullptr); ASSERT_NE(resourceManager.GetLoader(XCEngine::Resources::ResourceType::Mesh), nullptr);
XCEngine::Resources::MeshLoader meshLoader; XCEngine::Resources::MeshLoader meshLoader;
@@ -619,9 +656,12 @@ TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) {
Scene loadedScene; Scene loadedScene;
loadedScene.Load(backpackScenePath.string()); loadedScene.Load(backpackScenePath.string());
GameObject* backpackObject = loadedScene.Find("BackpackMesh"); std::vector<GameObject*> backpackObjects =
ASSERT_NE(backpackObject, nullptr); FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj");
ASSERT_EQ(backpackObjects.size(), 2u);
for (GameObject* backpackObject : backpackObjects) {
ASSERT_NE(backpackObject, nullptr);
auto* meshFilter = backpackObject->GetComponent<MeshFilterComponent>(); auto* meshFilter = backpackObject->GetComponent<MeshFilterComponent>();
auto* meshRenderer = backpackObject->GetComponent<MeshRendererComponent>(); auto* meshRenderer = backpackObject->GetComponent<MeshRendererComponent>();
ASSERT_NE(meshFilter, nullptr); ASSERT_NE(meshFilter, nullptr);
@@ -633,6 +673,7 @@ TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) {
EXPECT_GT(meshFilter->GetMesh()->GetMaterials().Size(), 0u); EXPECT_GT(meshFilter->GetMesh()->GetMaterials().Size(), 0u);
EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj"); EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj");
} }
}
TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) { TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) {
namespace fs = std::filesystem; namespace fs = std::filesystem;
@@ -645,9 +686,384 @@ TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) {
Scene loadedScene; Scene loadedScene;
loadedScene.Load(mainScenePath.string()); loadedScene.Load(mainScenePath.string());
EXPECT_NE(loadedScene.Find("Main Camera"), nullptr); EXPECT_NE(loadedScene.Find("Camera"), nullptr);
EXPECT_NE(loadedScene.Find("Directional Light"), nullptr); EXPECT_NE(loadedScene.Find("Light"), nullptr);
EXPECT_NE(loadedScene.Find("Cube"), nullptr);
EXPECT_EQ(loadedScene.Find("BackpackMesh"), 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 } // namespace

View File

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