diff --git a/Source/Engine/Content/SceneReference.h b/Source/Engine/Content/SceneReference.h index d03fff7d5..ffa696681 100644 --- a/Source/Engine/Content/SceneReference.h +++ b/Source/Engine/Content/SceneReference.h @@ -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<> diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index 629c44096..73fab889f 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -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 _sceneActions; CriticalSection _sceneActionsLocker; DateTime _lastSceneLoadTime(0); + Array _preloadedScenes; #if USE_EDITOR Array ScriptsReloadObjects; #endif @@ -290,6 +308,7 @@ Delegate Level::SceneLoaded; Delegate Level::SceneLoadError; Delegate Level::SceneUnloading; Delegate Level::SceneUnloaded; +Delegate Level::ScenePreloaded; #if USE_EDITOR Action Level::ScriptsReloadStart; Action Level::ScriptsReload; @@ -523,8 +542,8 @@ public: AssetReference 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(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(id, sceneAsset, true)); + _sceneActions.Enqueue(New(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(id); + if (sceneAsset == nullptr) + { + LOG(Error, "Cannot load scene asset."); + return true; + } + + ScopeLock lock(_sceneActionsLocker); + _sceneActions.Enqueue(New(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; } diff --git a/Source/Engine/Level/Level.cs b/Source/Engine/Level/Level.cs index a3794c60f..5aa86de55 100644 --- a/Source/Engine/Level/Level.cs +++ b/Source/Engine/Level/Level.cs @@ -14,7 +14,7 @@ namespace FlaxEngine public static bool ChangeSceneAsync(Guid sceneAssetId) { UnloadAllScenesAsync(); - return LoadSceneAsync(sceneAssetId); + return Internal_LoadSceneAsync(ref sceneAssetId); } /// @@ -34,7 +34,7 @@ namespace FlaxEngine /// True if action fails (given asset is not a scene asset, missing data, scene loading error), otherwise false. public static bool LoadScene(SceneReference sceneAsset) { - return LoadScene(sceneAsset.ID); + return Internal_LoadScene(ref sceneAsset.ID); } /// @@ -44,7 +44,30 @@ namespace FlaxEngine /// True if failed (given asset is not a scene asset, missing data), otherwise false. public static bool LoadSceneAsync(SceneReference sceneAsset) { - return LoadSceneAsync(sceneAsset.ID); + return Internal_LoadSceneAsync(ref sceneAsset.ID); + } + + /// + /// 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 or to finish loading preloaded scene and add it to the level. + /// Preloaded scene can be normally unloaded via to release resources if it's not needed anymore. + /// + /// The asset with the scene to preload. + /// True if preloading cannot be done, otherwise false. + public static bool PreloadSceneAsync(SceneReference sceneAsset) + { + return Internal_PreloadSceneAsync(ref sceneAsset.ID); + } + + /// + /// Unloads given scene. + /// + /// The asset with the scene to unload. + /// True if action cannot be done, otherwise false. + public static bool UnloadScene(SceneReference sceneAsset) + { + return Internal_UnloadScene(ref sceneAsset.ID); } /// diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index e3d43b7c5..50b183683 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -222,6 +222,11 @@ public: /// API_EVENT() static Delegate SceneUnloaded; + /// + /// Fired when scene gets preloaded (loaded but not yet added to the level). + /// + API_EVENT() static Delegate ScenePreloaded; + #if USE_EDITOR /// @@ -316,35 +321,52 @@ public: /// True if loading cannot be done, otherwise false. API_FUNCTION() static bool LoadSceneAsync(const Guid& id); + /// + /// 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 or to finish loading preloaded scene and add it to the level. + /// Preloaded scene can be normally unloaded via to release resources if it's not needed anymore. + /// + /// Scene ID + /// True if preloading cannot be done, otherwise false. + API_FUNCTION() static bool PreloadSceneAsync(const Guid& id); + /// /// Unloads given scene. /// + /// Scene ID + /// True if action cannot be done, otherwise false. + API_FUNCTION() static bool UnloadScene(const Guid& id); + + /// + /// Unloads given scene. + /// + /// Scene /// True if action cannot be done, otherwise false. API_FUNCTION() static bool UnloadScene(Scene* scene); /// /// Unloads given scene. Done in the background. /// + /// Scene API_FUNCTION() static void UnloadSceneAsync(Scene* scene); /// - /// Unloads all scenes. + /// Unloads all scenes. Cancels all scene preloading. /// /// True if action cannot be done, otherwise false. API_FUNCTION() static bool UnloadAllScenes(); /// - /// Unloads all scenes. Done in the background. + /// Unloads all scenes. Done in the background. Cancels all scene preloading. /// API_FUNCTION() static void UnloadAllScenesAsync(); #if USE_EDITOR - /// /// Reloads scripts. Done in the background. /// static void ReloadScriptsAsync(); - #endif public: