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);
///