Refactor level actions to support time budget and time slicing

This commit is contained in:
Wojtek Figat
2025-06-11 00:01:46 +02:00
parent d6b4992991
commit b50f3fcb64
4 changed files with 156 additions and 99 deletions

View File

@@ -43,7 +43,7 @@ public:
/// <summary>
/// Gets the total number of milliseconds.
/// </summary>
FORCE_INLINE double GetTotalMilliseconds() const
FORCE_INLINE float GetTotalMilliseconds() const
{
return (float)((_end - _start) * 1000.0);
}

View File

@@ -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<SceneAction*> _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<Scene*> Level::Scenes;
bool Level::TickEnabled = true;
float Level::StreamingFrameBudget = 0.3f;
Delegate<Actor*> Level::ActorSpawned;
Delegate<Actor*> Level::ActorDeleted;
Delegate<Actor*, Actor*> Level::ActorParentChanged;
@@ -394,40 +433,22 @@ class LoadSceneAction : public SceneAction
public:
Guid SceneId;
AssetReference<JsonAsset> 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<int32>((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<JsonAsset> 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<LoadSceneAction>(id, sceneAsset));
_sceneActions.Enqueue(New<LoadSceneAction>(id, sceneAsset, true));
return false;
}

View File

@@ -48,7 +48,12 @@ public:
/// <summary>
/// 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.
/// </summary>
API_FIELD() static bool TickEnabled;
API_FIELD(Attributes="DebugCommand") static bool TickEnabled;
/// <summary>
/// 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).
/// </summary>
API_FIELD(Attributes="DebugCommand") static float StreamingFrameBudget;
public:
/// <summary>
@@ -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);
};

View File

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