Add new Level.PreloadSceneAsync for streaming levels before they need to be loaded in

This commit is contained in:
2026-04-24 17:54:25 +02:00
parent 468e05c6e8
commit 615e847e01
4 changed files with 265 additions and 13 deletions
+11
View File
@@ -27,6 +27,17 @@ API_STRUCT(NoDefault) struct FLAXENGINE_API SceneReference
{ {
return ID != other.ID; return ID != other.ID;
} }
FORCE_INLINE SceneReference& operator=(const Guid& id)
{
ID = id;
return *this;
}
FORCE_INLINE operator Guid() const
{
return ID;
}
}; };
template<> template<>
+202 -6
View File
@@ -91,6 +91,7 @@ enum class SceneEventType
OnSceneLoadError = 5, OnSceneLoadError = 5,
OnSceneUnloading = 6, OnSceneUnloading = 6,
OnSceneUnloaded = 7, OnSceneUnloaded = 7,
OnScenePreloaded = 8,
}; };
enum class SceneResult enum class SceneResult
@@ -117,6 +118,11 @@ public:
{ {
return SceneResult::Failed; return SceneResult::Failed;
} }
virtual bool IsPreloading(const Guid* sceneId = nullptr)
{
return false;
}
}; };
#if USE_EDITOR #if USE_EDITOR
@@ -147,6 +153,13 @@ struct TimeSlicer
class SceneLoader class SceneLoader
{ {
public: public:
enum class Modes
{
Sync,
Async,
Preload,
};
struct Args struct Args
{ {
rapidjson_flax::Value& Data; rapidjson_flax::Value& Data;
@@ -172,6 +185,7 @@ public:
bool AsyncLoad; bool AsyncLoad;
bool AsyncJobs; bool AsyncJobs;
bool Preload;
Guid SceneId = Guid::Empty; Guid SceneId = Guid::Empty;
Scene* Scene = nullptr; Scene* Scene = nullptr;
float TotalTime = 0.0f; float TotalTime = 0.0f;
@@ -185,9 +199,10 @@ public:
SceneObjectsFactory::PrefabSyncData* PrefabSyncData = nullptr; SceneObjectsFactory::PrefabSyncData* PrefabSyncData = nullptr;
TimeSlicer StageSlicer; TimeSlicer StageSlicer;
SceneLoader(bool asyncLoad = false) SceneLoader(Modes mode = Modes::Sync)
: AsyncLoad(asyncLoad) : AsyncLoad(mode != Modes::Sync)
, AsyncJobs(JobSystem::GetThreadsCount() > 1) , AsyncJobs(JobSystem::GetThreadsCount() > 1)
, Preload(mode == Modes::Preload)
, Modifier(Cache::ISerializeModifier.GetUnscoped()) , Modifier(Cache::ISerializeModifier.GetUnscoped())
, Context(Modifier) , Context(Modifier)
{ {
@@ -221,6 +236,8 @@ public:
SceneResult OnInitialize(Args& args); SceneResult OnInitialize(Args& args);
SceneResult OnBeginPlay(Args& args); SceneResult OnBeginPlay(Args& args);
SceneResult OnEnd(Args& args); SceneResult OnEnd(Args& args);
static bool LoadPreloadedScene(::Scene* scene);
}; };
namespace LevelImpl namespace LevelImpl
@@ -228,6 +245,7 @@ namespace LevelImpl
Array<SceneAction*> _sceneActions; Array<SceneAction*> _sceneActions;
CriticalSection _sceneActionsLocker; CriticalSection _sceneActionsLocker;
DateTime _lastSceneLoadTime(0); DateTime _lastSceneLoadTime(0);
Array<Scene*> _preloadedScenes;
#if USE_EDITOR #if USE_EDITOR
Array<ScriptsReloadObject> ScriptsReloadObjects; Array<ScriptsReloadObject> ScriptsReloadObjects;
#endif #endif
@@ -290,6 +308,7 @@ Delegate<Scene*, const Guid&> Level::SceneLoaded;
Delegate<Scene*, const Guid&> Level::SceneLoadError; Delegate<Scene*, const Guid&> Level::SceneLoadError;
Delegate<Scene*, const Guid&> Level::SceneUnloading; Delegate<Scene*, const Guid&> Level::SceneUnloading;
Delegate<Scene*, const Guid&> Level::SceneUnloaded; Delegate<Scene*, const Guid&> Level::SceneUnloaded;
Delegate<Scene*, const Guid&> Level::ScenePreloaded;
#if USE_EDITOR #if USE_EDITOR
Action Level::ScriptsReloadStart; Action Level::ScriptsReloadStart;
Action Level::ScriptsReload; Action Level::ScriptsReload;
@@ -523,8 +542,8 @@ public:
AssetReference<JsonAsset> SceneAsset; AssetReference<JsonAsset> SceneAsset;
SceneLoader Loader; SceneLoader Loader;
LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset, bool async) LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset, SceneLoader::Modes mode)
: Loader(async) : Loader(mode)
{ {
SceneId = sceneId; SceneId = sceneId;
SceneAsset = sceneAsset; SceneAsset = sceneAsset;
@@ -538,6 +557,11 @@ public:
return SceneResult::Wait; return SceneResult::Wait;
return LevelImpl::loadScene(Loader, SceneAsset, &context.TimeBudget); return LevelImpl::loadScene(Loader, SceneAsset, &context.TimeBudget);
} }
bool IsPreloading(const Guid* sceneId) override
{
return Loader.Preload && (!sceneId || *sceneId == SceneId);
}
}; };
class UnloadSceneAction : public SceneAction class UnloadSceneAction : public SceneAction
@@ -817,6 +841,9 @@ void LevelImpl::CallSceneEvent(SceneEventType eventType, Scene* scene, Guid scen
case SceneEventType::OnSceneUnloaded: case SceneEventType::OnSceneUnloaded:
Level::SceneUnloaded(scene, sceneId); Level::SceneUnloaded(scene, sceneId);
break; break;
case SceneEventType::OnScenePreloaded:
Level::ScenePreloaded(scene, sceneId);
break;
} }
} }
@@ -953,6 +980,7 @@ bool LevelImpl::unloadScene(Scene* scene)
scene->EndPlay(); scene->EndPlay();
// Remove from scenes list // Remove from scenes list
_preloadedScenes.Remove(scene);
Level::Scenes.Remove(scene); Level::Scenes.Remove(scene);
// Fire event // Fire event
@@ -967,12 +995,36 @@ bool LevelImpl::unloadScene(Scene* scene)
bool LevelImpl::unloadScenes() bool LevelImpl::unloadScenes()
{ {
PROFILE_MEM(Level); PROFILE_MEM(Level);
// Preloading actions
_sceneActionsLocker.Lock();
for (int32 i = _sceneActions.Count() - 1; i >= 0; i--)
{
auto action = _sceneActions[i];
if (action->IsPreloading())
{
_sceneActions.RemoveAtKeepOrder(i--);
Delete(action);
}
}
_sceneActionsLocker.Unlock();
// Preloaded scenes
auto preloadedScenes = _preloadedScenes;
for (auto scene : preloadedScenes)
{
if (unloadScene(scene))
return true;
}
// Active scenes
auto scenes = Level::Scenes; auto scenes = Level::Scenes;
for (int32 i = scenes.Count() - 1; i >= 0; i--) for (int32 i = scenes.Count() - 1; i >= 0; i--)
{ {
if (unloadScene(scenes[i])) if (unloadScene(scenes[i]))
return true; return true;
} }
return false; return false;
} }
@@ -1044,7 +1096,7 @@ SceneResult LevelImpl::loadScene(SceneLoader& loader, rapidjson_flax::Value& dat
const float delta = time.GetTotalSeconds(); const float delta = time.GetTotalSeconds();
loader.TotalTime += delta; loader.TotalTime += delta;
timeLeft -= delta; timeLeft -= delta;
if (timeLeft < 0.0f && result == SceneResult::Success) if (timeLeft < 0.0f && result == SceneResult::Success && loader.Stage != SceneLoader::Stages::Loaded)
{ {
result = SceneResult::Wait; result = SceneResult::Wait;
break; break;
@@ -1391,6 +1443,15 @@ SceneResult SceneLoader::OnInitialize(Args& args)
SceneResult SceneLoader::OnBeginPlay(Args& args) SceneResult SceneLoader::OnBeginPlay(Args& args)
{ {
if (Preload)
{
// Scene got preloaded
_preloadedScenes.Add(Scene);
CallSceneEvent(SceneEventType::OnScenePreloaded, Scene, SceneId);
LOG(Info, "Scene pre-loaded in {}ms ({} frames)", (int32)(TotalTime * 1000.0), Engine::UpdateCount - StartFrame);
Stage = Stages::Loaded;
return SceneResult::Success;
}
PROFILE_CPU_NAMED("BeginPlay"); PROFILE_CPU_NAMED("BeginPlay");
ASSERT_LOW_LAYER(IsInMainThread()); ASSERT_LOW_LAYER(IsInMainThread());
@@ -1449,6 +1510,24 @@ SceneResult SceneLoader::OnEnd(Args& args)
return SceneResult::Success; return SceneResult::Success;
} }
bool SceneLoader::LoadPreloadedScene(::Scene* scene)
{
PROFILE_CPU_NAMED("BeginPlay");
ASSERT_LOW_LAYER(IsInMainThread());
// Link scene
ScopeLock lock(Level::ScenesLock);
_preloadedScenes.Remove(scene);
Level::Scenes.Add(scene);
// Start the game for scene objects
SceneBeginData beginData;
scene->BeginPlay(&beginData);
beginData.OnDone();
return false;
}
bool LevelImpl::saveScene(Scene* scene) bool LevelImpl::saveScene(Scene* scene)
{ {
#if USE_EDITOR #if USE_EDITOR
@@ -1657,6 +1736,33 @@ bool Level::LoadScene(const Guid& id)
return true; return true;
} }
// Check preloaded scenes
for (auto scene : _preloadedScenes)
{
if (scene->GetID() == id)
return SceneLoader::LoadPreloadedScene(scene);
}
// Check if scene is during preloading
_sceneActionsLocker.Lock();
for (auto action : _sceneActions)
{
if (action->IsPreloading(&id))
{
// Turn preloading into loading and run it now
auto loadAction = (LoadSceneAction*)action;
loadAction->Loader.Preload = false;
if (loadScene(loadAction->Loader, loadAction->SceneAsset) != SceneResult::Success)
{
LOG(Error, "Failed to deserialize scene {0}", id);
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, id);
return true;
}
return false;
}
}
_sceneActionsLocker.Unlock();
// Preload scene asset // Preload scene asset
const auto sceneAsset = Content::LoadAsync<JsonAsset>(id); const auto sceneAsset = Content::LoadAsync<JsonAsset>(id);
if (sceneAsset == nullptr) if (sceneAsset == nullptr)
@@ -1705,8 +1811,98 @@ bool Level::LoadSceneAsync(const Guid& id)
return true; return true;
} }
// Check preloaded scenes
// TODO: if scene loader gets time-slicing to begin play then make a new action to perform it in async
for (auto scene : _preloadedScenes)
{
if (scene->GetID() == id)
return SceneLoader::LoadPreloadedScene(scene);
}
// Check if scene is during preloading
_sceneActionsLocker.Lock();
for (auto action : _sceneActions)
{
if (action->IsPreloading(&id))
{
// Turn preloading into loading and let it load this scene (it might be in progress already)
auto loadAction = (LoadSceneAction*)action;
loadAction->Loader.Preload = false;
return false;
}
}
_sceneActionsLocker.Unlock();
ScopeLock lock(_sceneActionsLocker); ScopeLock lock(_sceneActionsLocker);
_sceneActions.Enqueue(New<LoadSceneAction>(id, sceneAsset, true)); _sceneActions.Enqueue(New<LoadSceneAction>(id, sceneAsset, SceneLoader::Modes::Async));
return false;
}
bool Level::PreloadSceneAsync(const Guid& id)
{
if (!id.IsValid())
{
Log::ArgumentException();
return true;
}
// Check preloaded scenes
for (auto scene : _preloadedScenes)
{
if (scene->GetID() == id)
return false; // Already preloaded
}
// Check queue
_sceneActionsLocker.Lock();
for (auto action : _sceneActions)
{
if (action->IsPreloading(&id))
return false; // Already preloading
}
_sceneActionsLocker.Unlock();
// Preload scene asset
const auto sceneAsset = Content::LoadAsync<JsonAsset>(id);
if (sceneAsset == nullptr)
{
LOG(Error, "Cannot load scene asset.");
return true;
}
ScopeLock lock(_sceneActionsLocker);
_sceneActions.Enqueue(New<LoadSceneAction>(id, sceneAsset, SceneLoader::Modes::Preload));
return false;
}
bool Level::UnloadScene(const Guid& id)
{
// Try with already loaded scene
if (Scene* scene = FindScene(id))
return unloadScene(scene);
// Check preloaded scenes
for (auto scene : _preloadedScenes)
{
if (scene->GetID() == id)
return unloadScene(scene);
}
// Check loading queue
_sceneActionsLocker.Lock();
for (int32 i = _sceneActions.Count() - 1; i >= 0; i--)
{
auto action = _sceneActions[i];
if (action->IsPreloading(&id))
{
_sceneActions.RemoveAtKeepOrder(i--);
Delete(action);
break;
}
}
_sceneActionsLocker.Unlock();
return false; return false;
} }
+26 -3
View File
@@ -14,7 +14,7 @@ namespace FlaxEngine
public static bool ChangeSceneAsync(Guid sceneAssetId) public static bool ChangeSceneAsync(Guid sceneAssetId)
{ {
UnloadAllScenesAsync(); UnloadAllScenesAsync();
return LoadSceneAsync(sceneAssetId); return Internal_LoadSceneAsync(ref sceneAssetId);
} }
/// <summary> /// <summary>
@@ -34,7 +34,7 @@ namespace FlaxEngine
/// <returns>True if action fails (given asset is not a scene asset, missing data, scene loading error), otherwise false.</returns> /// <returns>True if action fails (given asset is not a scene asset, missing data, scene loading error), otherwise false.</returns>
public static bool LoadScene(SceneReference sceneAsset) public static bool LoadScene(SceneReference sceneAsset)
{ {
return LoadScene(sceneAsset.ID); return Internal_LoadScene(ref sceneAsset.ID);
} }
/// <summary> /// <summary>
@@ -44,7 +44,30 @@ namespace FlaxEngine
/// <returns>True if failed (given asset is not a scene asset, missing data), otherwise false.</returns> /// <returns>True if failed (given asset is not a scene asset, missing data), otherwise false.</returns>
public static bool LoadSceneAsync(SceneReference sceneAsset) public static bool LoadSceneAsync(SceneReference sceneAsset)
{ {
return LoadSceneAsync(sceneAsset.ID); return Internal_LoadSceneAsync(ref sceneAsset.ID);
}
/// <summary>
/// Begins preloading scene in the background. Intended to use by scene streaming systems that want to prepare scene but not add to the level yet.
/// Loads scene asset, deserializes actors and scripts, setup objects hierarchy with transformations and initializes them (incl. calling OnAwake for scripts).
/// Preloaded scene is not added to the loaded scenes and not active in the game (no BeginPlay/OnEnable/OnStart called yet). Use <see cref="LoadScene(Guid)"/> or <see cref="LoadSceneAsync(Guid)"/> to finish loading preloaded scene and add it to the level.
/// Preloaded scene can be normally unloaded via <see cref="UnloadScene(Guid)"/> to release resources if it's not needed anymore.
/// </summary>
/// <param name="sceneAsset">The asset with the scene to preload.</param>
/// <returns>True if preloading cannot be done, otherwise false.</returns>
public static bool PreloadSceneAsync(SceneReference sceneAsset)
{
return Internal_PreloadSceneAsync(ref sceneAsset.ID);
}
/// <summary>
/// Unloads given scene.
/// </summary>
/// <param name="sceneAsset">The asset with the scene to unload.</param>
/// <returns>True if action cannot be done, otherwise false.</returns>
public static bool UnloadScene(SceneReference sceneAsset)
{
return Internal_UnloadScene(ref sceneAsset.ID);
} }
/// <summary> /// <summary>
+26 -4
View File
@@ -222,6 +222,11 @@ public:
/// </summary> /// </summary>
API_EVENT() static Delegate<Scene*, const Guid&> SceneUnloaded; API_EVENT() static Delegate<Scene*, const Guid&> SceneUnloaded;
/// <summary>
/// Fired when scene gets preloaded (loaded but not yet added to the level).
/// </summary>
API_EVENT() static Delegate<Scene*, const Guid&> ScenePreloaded;
#if USE_EDITOR #if USE_EDITOR
/// <summary> /// <summary>
@@ -316,35 +321,52 @@ public:
/// <returns>True if loading cannot be done, otherwise false.</returns> /// <returns>True if loading cannot be done, otherwise false.</returns>
API_FUNCTION() static bool LoadSceneAsync(const Guid& id); API_FUNCTION() static bool LoadSceneAsync(const Guid& id);
/// <summary>
/// Begins preloading scene in the background. Intended to use by scene streaming systems that want to prepare scene but not add to the level yet.
/// Loads scene asset, deserializes actors and scripts, setup objects hierarchy with transformations and initializes them (incl. calling OnAwake for scripts).
/// Preloaded scene is not added to the loaded scenes and not active in the game (no BeginPlay/OnEnable/OnStart called yet). Use <see cref="LoadScene(Guid)"/> or <see cref="LoadSceneAsync(Guid)"/> to finish loading preloaded scene and add it to the level.
/// Preloaded scene can be normally unloaded via <see cref="UnloadScene(Guid)"/> to release resources if it's not needed anymore.
/// </summary>
/// <param name="id">Scene ID</param>
/// <returns>True if preloading cannot be done, otherwise false.</returns>
API_FUNCTION() static bool PreloadSceneAsync(const Guid& id);
/// <summary> /// <summary>
/// Unloads given scene. /// Unloads given scene.
/// </summary> /// </summary>
/// <param name="id">Scene ID</param>
/// <returns>True if action cannot be done, otherwise false.</returns>
API_FUNCTION() static bool UnloadScene(const Guid& id);
/// <summary>
/// Unloads given scene.
/// </summary>
/// <param name="scene">Scene</param>
/// <returns>True if action cannot be done, otherwise false.</returns> /// <returns>True if action cannot be done, otherwise false.</returns>
API_FUNCTION() static bool UnloadScene(Scene* scene); API_FUNCTION() static bool UnloadScene(Scene* scene);
/// <summary> /// <summary>
/// Unloads given scene. Done in the background. /// Unloads given scene. Done in the background.
/// </summary> /// </summary>
/// <param name="scene">Scene</param>
API_FUNCTION() static void UnloadSceneAsync(Scene* scene); API_FUNCTION() static void UnloadSceneAsync(Scene* scene);
/// <summary> /// <summary>
/// Unloads all scenes. /// Unloads all scenes. Cancels all scene preloading.
/// </summary> /// </summary>
/// <returns>True if action cannot be done, otherwise false.</returns> /// <returns>True if action cannot be done, otherwise false.</returns>
API_FUNCTION() static bool UnloadAllScenes(); API_FUNCTION() static bool UnloadAllScenes();
/// <summary> /// <summary>
/// Unloads all scenes. Done in the background. /// Unloads all scenes. Done in the background. Cancels all scene preloading.
/// </summary> /// </summary>
API_FUNCTION() static void UnloadAllScenesAsync(); API_FUNCTION() static void UnloadAllScenesAsync();
#if USE_EDITOR #if USE_EDITOR
/// <summary> /// <summary>
/// Reloads scripts. Done in the background. /// Reloads scripts. Done in the background.
/// </summary> /// </summary>
static void ReloadScriptsAsync(); static void ReloadScriptsAsync();
#endif #endif
public: public: