diff --git a/Source/Engine/Core/Types/Stopwatch.h b/Source/Engine/Core/Types/Stopwatch.h index c909285af..d87df0f21 100644 --- a/Source/Engine/Core/Types/Stopwatch.h +++ b/Source/Engine/Core/Types/Stopwatch.h @@ -43,7 +43,7 @@ public: /// /// Gets the total number of milliseconds. /// - FORCE_INLINE double GetTotalMilliseconds() const + FORCE_INLINE float GetTotalMilliseconds() const { return (float)((_end - _start) * 1000.0); } diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index 1233282be..d3f1ccba3 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -18,16 +18,15 @@ #include "Engine/Debug/Exceptions/ArgumentNullException.h" #include "Engine/Debug/Exceptions/InvalidOperationException.h" #include "Engine/Debug/Exceptions/JsonParseException.h" +#include "Engine/Engine/Engine.h" #include "Engine/Engine/EngineService.h" #include "Engine/Threading/Threading.h" #include "Engine/Threading/JobSystem.h" #include "Engine/Platform/File.h" -#include "Engine/Platform/FileSystem.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Scripting/Script.h" #include "Engine/Engine/Time.h" -#include "Engine/Scripting/ManagedCLR/MAssembly.h" #include "Engine/Scripting/ManagedCLR/MClass.h" #include "Engine/Scripting/ManagedCLR/MDomain.h" #include "Engine/Scripting/ManagedCLR/MException.h" @@ -78,6 +77,13 @@ enum class SceneEventType OnSceneUnloaded = 7, }; +enum class SceneResult +{ + Success, + Failed, + Wait, +}; + class SceneAction { public: @@ -85,14 +91,15 @@ public: { } - virtual bool CanDo() const + struct Context { - return true; - } + // Amount of seconds that action can take to run within a budget. + float TimeBudget = MAX_float; + }; - virtual bool Do() const + virtual SceneResult Do(Context& context) { - return true; + return SceneResult::Failed; } }; @@ -107,6 +114,33 @@ struct ScriptsReloadObject #endif +// Async map loading utility for state tracking and synchronization of various load stages. +class SceneLoader +{ +public: + enum Stages + { + Init, + Spawn, + SetupPrefabs, + Deserialize, + SetupTransforms, + BeginPlay, + Loaded, + } Stage = Init; + bool AsyncLoad; + bool AsyncJobs; + float TotalTime = 0.0f; + + SceneLoader(bool asyncLoad = false) + : AsyncLoad(true) + , AsyncJobs(JobSystem::GetThreadsCount() > 1) + { + } + + SceneResult Tick(rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene, const String* assetPath, float* timeBudget); +}; + namespace LevelImpl { Array _sceneActions; @@ -119,6 +153,10 @@ namespace LevelImpl void CallSceneEvent(SceneEventType eventType, Scene* scene, Guid sceneId); void flushActions(); + SceneResult loadScene(SceneLoader& loader, JsonAsset* sceneAsset); + SceneResult loadScene(SceneLoader& loader, const BytesContainer& sceneData, Scene** outScene = nullptr); + SceneResult loadScene(SceneLoader& loader, rapidjson_flax::Document& document, Scene** outScene = nullptr); + SceneResult loadScene(SceneLoader& loader, rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene = nullptr, const String* assetPath = nullptr, float* timeBudget = nullptr); bool unloadScene(Scene* scene); bool unloadScenes(); bool saveScene(Scene* scene); @@ -151,6 +189,7 @@ LevelService LevelServiceInstanceService; CriticalSection Level::ScenesLock; Array Level::Scenes; bool Level::TickEnabled = true; +float Level::StreamingFrameBudget = 0.3f; Delegate Level::ActorSpawned; Delegate Level::ActorDeleted; Delegate Level::ActorParentChanged; @@ -394,40 +433,22 @@ class LoadSceneAction : public SceneAction public: Guid SceneId; AssetReference SceneAsset; + SceneLoader Loader; - LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset) + LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset, bool async) + : Loader(async) { SceneId = sceneId; SceneAsset = sceneAsset; } - bool CanDo() const override + SceneResult Do(Context& context) override { - return SceneAsset == nullptr || SceneAsset->IsLoaded(); - } - - bool Do() const override - { - // Now to deserialize scene in a proper way we need to load scripting - if (!Scripting::IsEveryAssemblyLoaded()) - { - LOG(Error, "Scripts must be compiled without any errors in order to load a scene."); -#if USE_EDITOR - Platform::Error(TEXT("Scripts must be compiled without any errors in order to load a scene. Please fix it.")); -#endif - CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); - return true; - } - - // Load scene - if (Level::loadScene(SceneAsset)) - { - LOG(Error, "Failed to deserialize scene {0}", SceneId); - CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); - return true; - } - - return false; + if (SceneAsset == nullptr) + return SceneResult::Failed; + if (!SceneAsset->IsLoaded()) + return SceneResult::Wait; + return LevelImpl::loadScene(Loader, SceneAsset); } }; @@ -441,12 +462,12 @@ public: TargetScene = scene->GetID(); } - bool Do() const override + SceneResult Do(Context& context) override { auto scene = Level::FindScene(TargetScene); if (!scene) - return true; - return unloadScene(scene); + return SceneResult::Failed; + return unloadScene(scene) ? SceneResult::Failed : SceneResult::Success; } }; @@ -457,9 +478,9 @@ public: { } - bool Do() const override + SceneResult Do(Context& context) override { - return unloadScenes(); + return unloadScenes() ? SceneResult::Failed : SceneResult::Success; } }; @@ -475,14 +496,14 @@ public: PrettyJson = prettyJson; } - bool Do() const override + SceneResult Do(Context& context) override { if (saveScene(TargetScene)) { LOG(Error, "Failed to save scene {0}", TargetScene ? TargetScene->GetName() : String::Empty); - return true; + return SceneResult::Failed; } - return false; + return SceneResult::Success; } }; @@ -495,7 +516,7 @@ public: { } - bool Do() const override + SceneResult Do(Context& context) override { // Reloading scripts workflow: // - save scenes (to temporary files) @@ -556,7 +577,7 @@ public: { LOG(Error, "Failed to save scene '{0}' for scripts reload.", scenes[i].Name); CallSceneEvent(SceneEventType::OnSceneSaveError, scene, scene->GetID()); - return true; + return SceneResult::Failed; } CallSceneEvent(SceneEventType::OnSceneSaved, scene, scene->GetID()); } @@ -601,16 +622,17 @@ public: } if (document.HasParseError()) { - LOG(Error, "Failed to deserialize scene {0}. Result: {1}", scenes[i].Name, GetParseError_En(document.GetParseError())); - return true; + LOG(Error, "Failed to deserialize scene {0}. SceneResult: {1}", scenes[i].Name, GetParseError_En(document.GetParseError())); + return SceneResult::Failed; } // Load scene - if (Level::loadScene(document)) + SceneLoader loader; + if (LevelImpl::loadScene(loader, document) != SceneResult::Success) { LOG(Error, "Failed to deserialize scene {0}", scenes[i].Name); CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, scenes[i].ID); - return true; + return SceneResult::Failed; } } scenes.Resize(0); @@ -619,7 +641,7 @@ public: LOG(Info, "Scripts reloading end. Total time: {0}ms", static_cast((DateTime::NowUTC() - startTime).GetTotalMilliseconds())); Level::ScriptsReloadEnd(); - return false; + return SceneResult::Success; } }; @@ -651,9 +673,9 @@ public: { } - bool Do() const override + SceneResult Do(Context& context) override { - return spawnActor(TargetActor, ParentActor); + return spawnActor(TargetActor, ParentActor) ? SceneResult::Failed : SceneResult::Success; } }; @@ -667,9 +689,9 @@ public: { } - bool Do() const override + SceneResult Do(Context& context) override { - return deleteActor(TargetActor); + return deleteActor(TargetActor) ? SceneResult::Failed : SceneResult::Success; } }; @@ -767,13 +789,29 @@ void Level::callActorEvent(ActorEventType eventType, Actor* a, Actor* b) void LevelImpl::flushActions() { - ScopeLock lock(_sceneActionsLocker); + // Calculate time budget for the streaming (relative to the game frame rate to scale across different devices) + SceneAction::Context context; + float targetFps = 60; + if (Time::UpdateFPS > ZeroTolerance) + targetFps = Time::UpdateFPS; + else if (Engine::GetFramesPerSecond() > 0) + targetFps = (float)Engine::GetFramesPerSecond(); + context.TimeBudget = Level::StreamingFrameBudget / targetFps; - while (_sceneActions.HasItems() && _sceneActions.First()->CanDo()) + // Runs actions in order + ScopeLock lock(_sceneActionsLocker); + for (int32 i = 0; i < _sceneActions.Count() && context.TimeBudget >= 0.0; i++) { - const auto action = _sceneActions.Dequeue(); - action->Do(); - Delete(action); + auto action = _sceneActions[0]; + Stopwatch time; + auto result = action->Do(context); + time.Stop(); + context.TimeBudget -= time.GetTotalSeconds(); + if (result != SceneResult::Wait) + { + _sceneActions.RemoveAtKeepOrder(i--); + Delete(action); + } } } @@ -823,25 +861,25 @@ bool LevelImpl::unloadScenes() return false; } -bool Level::loadScene(JsonAsset* sceneAsset) +SceneResult LevelImpl::loadScene(SceneLoader& loader, JsonAsset* sceneAsset) { // Keep reference to the asset (prevent unloading during action) AssetReference ref = sceneAsset; if (sceneAsset == nullptr || sceneAsset->WaitForLoaded()) { LOG(Error, "Cannot load scene asset."); - return true; + return SceneResult::Failed; } - return loadScene(*sceneAsset->Data, sceneAsset->DataEngineBuild, nullptr, &sceneAsset->GetPath()); + return loadScene(loader, *sceneAsset->Data, sceneAsset->DataEngineBuild, nullptr, &sceneAsset->GetPath()); } -bool Level::loadScene(const BytesContainer& sceneData, Scene** outScene) +SceneResult LevelImpl::loadScene(SceneLoader& loader, const BytesContainer& sceneData, Scene** outScene) { if (sceneData.IsInvalid()) { LOG(Error, "Missing scene data."); - return true; + return SceneResult::Failed; } PROFILE_MEM(Level); @@ -854,34 +892,48 @@ bool Level::loadScene(const BytesContainer& sceneData, Scene** outScene) if (document.HasParseError()) { Log::JsonParseException(document.GetParseError(), document.GetErrorOffset()); - return true; + return SceneResult::Failed; } - ScopeLock lock(ScenesLock); - return loadScene(document, outScene); + ScopeLock lock(Level::ScenesLock); + return loadScene(loader, document, outScene); } -bool Level::loadScene(rapidjson_flax::Document& document, Scene** outScene) +SceneResult LevelImpl::loadScene(SceneLoader& loader, rapidjson_flax::Document& document, Scene** outScene) { auto data = document.FindMember("Data"); if (data == document.MemberEnd()) { LOG(Error, "Missing Data member."); - return true; + return SceneResult::Failed; } const int32 saveEngineBuild = JsonTools::GetInt(document, "EngineBuild", 0); - return loadScene(data->value, saveEngineBuild, outScene); + return loadScene(loader, data->value, saveEngineBuild, outScene); } -bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene, const String* assetPath) +SceneResult LevelImpl::loadScene(SceneLoader& loader, rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene, const String* assetPath, float* timeBudget) { PROFILE_CPU_NAMED("Level.LoadScene"); PROFILE_MEM(Level); - if (outScene) - *outScene = nullptr; #if USE_EDITOR ContentDeprecated::Clear(); #endif + SceneResult result = SceneResult::Success; + while ((!timeBudget || *timeBudget > 0.0f) && loader.Stage != SceneLoader::Loaded && result == SceneResult::Success) + { + Stopwatch time; + result = loader.Tick(data, engineBuild, outScene, assetPath, timeBudget); + time.Stop(); + const float delta = time.GetTotalSeconds(); + loader.TotalTime += delta; + if (timeBudget) + *timeBudget -= delta; + } + return result; +} + +SceneResult SceneLoader::Tick(rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene, const String* assetPath, float* timeBudget) +{ LOG(Info, "Loading scene..."); Stopwatch stopwatch; _lastSceneLoadTime = DateTime::Now(); @@ -900,19 +952,19 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou MessageBox::Show(TEXT("Failed to load scripts.\n\nCannot load scene without game script modules.\n\nSee logs for more info."), TEXT("Missing game modules"), MessageBoxButtons::OK, MessageBoxIcon::Error); } #endif - return true; + return SceneResult::Failed; } // Peek meta if (engineBuild < 6000) { LOG(Error, "Invalid serialized engine build."); - return true; + return SceneResult::Failed; } if (!data.IsArray()) { LOG(Error, "Invalid Data member."); - return true; + return SceneResult::Failed; } // Peek scene node value (it's the first actor serialized) @@ -920,16 +972,16 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou if (!sceneId.IsValid()) { LOG(Error, "Invalid scene id."); - return true; + return SceneResult::Failed; } auto modifier = Cache::ISerializeModifier.Get(); modifier->EngineBuild = engineBuild; // Skip is that scene is already loaded - if (FindScene(sceneId) != nullptr) + if (Level::FindScene(sceneId) != nullptr) { LOG(Info, "Scene {0} is already loaded.", sceneId); - return false; + return SceneResult::Failed; } // Create scene actor @@ -958,7 +1010,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou SceneObject** objects = sceneObjects->Get(); if (context.Async) { - ScenesLock.Unlock(); // Unlock scenes from Main Thread so Job Threads can use it to safely setup actors hierarchy (see Actor::Deserialize) + Level::ScenesLock.Unlock(); // Unlock scenes from Main Thread so Job Threads can use it to safely setup actors hierarchy (see Actor::Deserialize) JobSystem::Execute([&](int32 i) { PROFILE_MEM(Level); @@ -979,7 +1031,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou else SceneObjectsFactory::HandleObjectDeserializationError(stream); }, dataCount - 1); - ScenesLock.Lock(); + Level::ScenesLock.Lock(); } else { @@ -1015,7 +1067,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou // TODO: - add _loadNoAsync flag to SceneObject or Actor to handle non-async loading for those types (eg. UIControl/UICanvas) if (context.Async) { - ScenesLock.Unlock(); // Unlock scenes from Main Thread so Job Threads can use it to safely setup actors hierarchy (see Actor::Deserialize) + Level::ScenesLock.Unlock(); // Unlock scenes from Main Thread so Job Threads can use it to safely setup actors hierarchy (see Actor::Deserialize) #if USE_EDITOR volatile int64 deprecated = 0; #endif @@ -1039,7 +1091,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou if (deprecated != 0) ContentDeprecated::Mark(); #endif - ScenesLock.Lock(); + Level::ScenesLock.Lock(); } else { @@ -1114,13 +1166,15 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou { PROFILE_CPU_NAMED("BeginPlay"); - ScopeLock lock(ScenesLock); - Scenes.Add(scene); + ScopeLock lock(Level::ScenesLock); + Level::Scenes.Add(scene); SceneBeginData beginData; scene->BeginPlay(&beginData); beginData.OnDone(); } + Stage = Loaded; + // Fire event CallSceneEvent(SceneEventType::OnSceneLoaded, scene, sceneId); @@ -1150,7 +1204,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou } #endif - return false; + return SceneResult::Success; } bool LevelImpl::saveScene(Scene* scene) @@ -1271,7 +1325,8 @@ bool LevelImpl::saveScene(Scene* scene, rapidjson_flax::StringBuffer& outBuffer, bool Level::SaveScene(Scene* scene, bool prettyJson) { ScopeLock lock(_sceneActionsLocker); - return SaveSceneAction(scene, prettyJson).Do(); + SceneAction::Context context; + return SaveSceneAction(scene, prettyJson).Do(context) != SceneResult::Success; } bool Level::SaveSceneToBytes(Scene* scene, rapidjson_flax::StringBuffer& outData, bool prettyJson) @@ -1317,9 +1372,10 @@ void Level::SaveSceneAsync(Scene* scene) bool Level::SaveAllScenes() { ScopeLock lock(_sceneActionsLocker); + SceneAction::Context context; for (int32 i = 0; i < Scenes.Count(); i++) { - if (SaveSceneAction(Scenes[i]).Do()) + if (SaveSceneAction(Scenes[i]).Do(context) != SceneResult::Success) return true; } return false; @@ -1369,7 +1425,8 @@ bool Level::LoadScene(const Guid& id) // Load scene ScopeLock lock(ScenesLock); - if (loadScene(sceneAsset)) + SceneLoader loader; + if (loadScene(loader, sceneAsset) != SceneResult::Success) { LOG(Error, "Failed to deserialize scene {0}", id); CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, id); @@ -1381,7 +1438,8 @@ bool Level::LoadScene(const Guid& id) Scene* Level::LoadSceneFromBytes(const BytesContainer& data) { Scene* scene = nullptr; - if (loadScene(data, &scene)) + SceneLoader loader; + if (loadScene(loader, data, &scene) != SceneResult::Success) { LOG(Error, "Failed to deserialize scene from bytes"); CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, Guid::Empty); @@ -1391,7 +1449,6 @@ Scene* Level::LoadSceneFromBytes(const BytesContainer& data) bool Level::LoadSceneAsync(const Guid& id) { - // Check ID if (!id.IsValid()) { Log::ArgumentException(); @@ -1407,7 +1464,7 @@ bool Level::LoadSceneAsync(const Guid& id) } ScopeLock lock(_sceneActionsLocker); - _sceneActions.Enqueue(New(id, sceneAsset)); + _sceneActions.Enqueue(New(id, sceneAsset, true)); return false; } diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index f07a20c51..597bc0a87 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -48,7 +48,12 @@ public: /// /// True if game objects (actors and scripts) can receive a tick during engine Update/LateUpdate/FixedUpdate events. Can be used to temporarily disable gameplay logic updating. /// - API_FIELD() static bool TickEnabled; + API_FIELD(Attributes="DebugCommand") static bool TickEnabled; + + /// + /// Fraction of the frame budget to limit time spent on levels streaming. For example, value of 0.3 means that 30% of frame time can be spent on levels loading within a single frame (eg. 0.3 at 60fps is 4.8ms budget). + /// + API_FIELD(Attributes="DebugCommand") static float StreamingFrameBudget; public: /// @@ -547,10 +552,4 @@ private: }; static void callActorEvent(ActorEventType eventType, Actor* a, Actor* b); - - // All loadScene assume that ScenesLock has been taken by the calling thread - static bool loadScene(JsonAsset* sceneAsset); - static bool loadScene(const BytesContainer& sceneData, Scene** outScene = nullptr); - static bool loadScene(rapidjson_flax::Document& document, Scene** outScene = nullptr); - static bool loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** outScene = nullptr, const String* assetPath = nullptr); }; diff --git a/Source/Engine/Level/Scene/Scene.h b/Source/Engine/Level/Scene/Scene.h index a34ebd592..f8f40b05a 100644 --- a/Source/Engine/Level/Scene/Scene.h +++ b/Source/Engine/Level/Scene/Scene.h @@ -19,6 +19,7 @@ API_CLASS() class FLAXENGINE_API Scene : public Actor { friend class Level; friend class ReloadScriptsAction; + friend class SceneLoader; DECLARE_SCENE_OBJECT(Scene); ///