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;
}
FORCE_INLINE SceneReference& operator=(const Guid& id)
{
ID = id;
return *this;
}
FORCE_INLINE operator Guid() const
{
return ID;
}
};
template<>
+202 -6
View File
@@ -91,6 +91,7 @@ enum class SceneEventType
OnSceneLoadError = 5,
OnSceneUnloading = 6,
OnSceneUnloaded = 7,
OnScenePreloaded = 8,
};
enum class SceneResult
@@ -117,6 +118,11 @@ public:
{
return SceneResult::Failed;
}
virtual bool IsPreloading(const Guid* sceneId = nullptr)
{
return false;
}
};
#if USE_EDITOR
@@ -147,6 +153,13 @@ struct TimeSlicer
class SceneLoader
{
public:
enum class Modes
{
Sync,
Async,
Preload,
};
struct Args
{
rapidjson_flax::Value& Data;
@@ -172,6 +185,7 @@ public:
bool AsyncLoad;
bool AsyncJobs;
bool Preload;
Guid SceneId = Guid::Empty;
Scene* Scene = nullptr;
float TotalTime = 0.0f;
@@ -185,9 +199,10 @@ public:
SceneObjectsFactory::PrefabSyncData* PrefabSyncData = nullptr;
TimeSlicer StageSlicer;
SceneLoader(bool asyncLoad = false)
: AsyncLoad(asyncLoad)
SceneLoader(Modes mode = Modes::Sync)
: AsyncLoad(mode != Modes::Sync)
, AsyncJobs(JobSystem::GetThreadsCount() > 1)
, Preload(mode == Modes::Preload)
, Modifier(Cache::ISerializeModifier.GetUnscoped())
, Context(Modifier)
{
@@ -221,6 +236,8 @@ public:
SceneResult OnInitialize(Args& args);
SceneResult OnBeginPlay(Args& args);
SceneResult OnEnd(Args& args);
static bool LoadPreloadedScene(::Scene* scene);
};
namespace LevelImpl
@@ -228,6 +245,7 @@ namespace LevelImpl
Array<SceneAction*> _sceneActions;
CriticalSection _sceneActionsLocker;
DateTime _lastSceneLoadTime(0);
Array<Scene*> _preloadedScenes;
#if USE_EDITOR
Array<ScriptsReloadObject> ScriptsReloadObjects;
#endif
@@ -290,6 +308,7 @@ Delegate<Scene*, const Guid&> Level::SceneLoaded;
Delegate<Scene*, const Guid&> Level::SceneLoadError;
Delegate<Scene*, const Guid&> Level::SceneUnloading;
Delegate<Scene*, const Guid&> Level::SceneUnloaded;
Delegate<Scene*, const Guid&> Level::ScenePreloaded;
#if USE_EDITOR
Action Level::ScriptsReloadStart;
Action Level::ScriptsReload;
@@ -523,8 +542,8 @@ public:
AssetReference<JsonAsset> SceneAsset;
SceneLoader Loader;
LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset, bool async)
: Loader(async)
LoadSceneAction(const Guid& sceneId, JsonAsset* sceneAsset, SceneLoader::Modes mode)
: Loader(mode)
{
SceneId = sceneId;
SceneAsset = sceneAsset;
@@ -538,6 +557,11 @@ public:
return SceneResult::Wait;
return LevelImpl::loadScene(Loader, SceneAsset, &context.TimeBudget);
}
bool IsPreloading(const Guid* sceneId) override
{
return Loader.Preload && (!sceneId || *sceneId == SceneId);
}
};
class UnloadSceneAction : public SceneAction
@@ -817,6 +841,9 @@ void LevelImpl::CallSceneEvent(SceneEventType eventType, Scene* scene, Guid scen
case SceneEventType::OnSceneUnloaded:
Level::SceneUnloaded(scene, sceneId);
break;
case SceneEventType::OnScenePreloaded:
Level::ScenePreloaded(scene, sceneId);
break;
}
}
@@ -953,6 +980,7 @@ bool LevelImpl::unloadScene(Scene* scene)
scene->EndPlay();
// Remove from scenes list
_preloadedScenes.Remove(scene);
Level::Scenes.Remove(scene);
// Fire event
@@ -967,12 +995,36 @@ bool LevelImpl::unloadScene(Scene* scene)
bool LevelImpl::unloadScenes()
{
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;
for (int32 i = scenes.Count() - 1; i >= 0; i--)
{
if (unloadScene(scenes[i]))
return true;
}
return false;
}
@@ -1044,7 +1096,7 @@ SceneResult LevelImpl::loadScene(SceneLoader& loader, rapidjson_flax::Value& dat
const float delta = time.GetTotalSeconds();
loader.TotalTime += 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;
break;
@@ -1391,6 +1443,15 @@ SceneResult SceneLoader::OnInitialize(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");
ASSERT_LOW_LAYER(IsInMainThread());
@@ -1449,6 +1510,24 @@ SceneResult SceneLoader::OnEnd(Args& args)
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)
{
#if USE_EDITOR
@@ -1657,6 +1736,33 @@ bool Level::LoadScene(const Guid& id)
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
const auto sceneAsset = Content::LoadAsync<JsonAsset>(id);
if (sceneAsset == nullptr)
@@ -1705,8 +1811,98 @@ bool Level::LoadSceneAsync(const Guid& id)
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);
_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;
}
+26 -3
View File
@@ -14,7 +14,7 @@ namespace FlaxEngine
public static bool ChangeSceneAsync(Guid sceneAssetId)
{
UnloadAllScenesAsync();
return LoadSceneAsync(sceneAssetId);
return Internal_LoadSceneAsync(ref sceneAssetId);
}
/// <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>
public static bool LoadScene(SceneReference sceneAsset)
{
return LoadScene(sceneAsset.ID);
return Internal_LoadScene(ref sceneAsset.ID);
}
/// <summary>
@@ -44,7 +44,30 @@ namespace FlaxEngine
/// <returns>True if failed (given asset is not a scene asset, missing data), otherwise false.</returns>
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>
+26 -4
View File
@@ -222,6 +222,11 @@ public:
/// </summary>
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
/// <summary>
@@ -316,35 +321,52 @@ public:
/// <returns>True if loading cannot be done, otherwise false.</returns>
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>
/// Unloads given scene.
/// </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>
API_FUNCTION() static bool UnloadScene(Scene* scene);
/// <summary>
/// Unloads given scene. Done in the background.
/// </summary>
/// <param name="scene">Scene</param>
API_FUNCTION() static void UnloadSceneAsync(Scene* scene);
/// <summary>
/// Unloads all scenes.
/// Unloads all scenes. Cancels all scene preloading.
/// </summary>
/// <returns>True if action cannot be done, otherwise false.</returns>
API_FUNCTION() static bool UnloadAllScenes();
/// <summary>
/// Unloads all scenes. Done in the background.
/// Unloads all scenes. Done in the background. Cancels all scene preloading.
/// </summary>
API_FUNCTION() static void UnloadAllScenesAsync();
#if USE_EDITOR
/// <summary>
/// Reloads scripts. Done in the background.
/// </summary>
static void ReloadScriptsAsync();
#endif
public: