Optimize scenes loading with Job System
This commit is contained in:
@@ -8,6 +8,7 @@ CollectionPoolCache<ISerializeModifier, Cache::ISerializeModifierClearCallback>
|
||||
void Cache::ISerializeModifierClearCallback(::ISerializeModifier* obj)
|
||||
{
|
||||
obj->EngineBuild = FLAXENGINE_VERSION_BUILD;
|
||||
obj->CurrentInstance = -1;
|
||||
obj->IdsMapping.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public:
|
||||
private:
|
||||
CollectionPoolCache* _pool;
|
||||
|
||||
ScopeCache(CollectionPoolCache* pool, T* value)
|
||||
FORCE_INLINE ScopeCache(CollectionPoolCache* pool, T* value)
|
||||
{
|
||||
_pool = pool;
|
||||
Value = value;
|
||||
@@ -71,7 +71,7 @@ public:
|
||||
|
||||
~ScopeCache()
|
||||
{
|
||||
_pool->Release(Value);
|
||||
_pool->Put(Value);
|
||||
}
|
||||
|
||||
T* operator->()
|
||||
@@ -99,13 +99,6 @@ private:
|
||||
CriticalSection _locker;
|
||||
Array<T*, InlinedAllocation<64>> _pool;
|
||||
|
||||
void Release(T* value)
|
||||
{
|
||||
_locker.Lock();
|
||||
_pool.Add(value);
|
||||
_locker.Unlock();
|
||||
}
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="CollectionPoolCache"/> class.
|
||||
@@ -120,7 +113,16 @@ public:
|
||||
/// Gets the collection instance from the pool. Can reuse the object from the pool or create a new one. Returns collection is always cleared and ready to use.
|
||||
/// </summary>
|
||||
/// <returns>The collection (cleared).</returns>
|
||||
ScopeCache Get()
|
||||
FORCE_INLINE ScopeCache Get()
|
||||
{
|
||||
return ScopeCache(this, GetUnscoped());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection instance from the pool. Can reuse the object from the pool or create a new one. Returns collection is always cleared and ready to use.
|
||||
/// </summary>
|
||||
/// <returns>The collection (cleared).</returns>
|
||||
T* GetUnscoped()
|
||||
{
|
||||
T* result;
|
||||
_locker.Lock();
|
||||
@@ -129,10 +131,18 @@ public:
|
||||
else
|
||||
result = CreateCallback();
|
||||
_locker.Unlock();
|
||||
|
||||
ClearCallback(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return ScopeCache(this, result);
|
||||
/// <summary>
|
||||
/// Puts the collection value back to the pool.
|
||||
/// </summary>
|
||||
void Put(T* value)
|
||||
{
|
||||
_locker.Lock();
|
||||
_pool.Add(value);
|
||||
_locker.Unlock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -785,12 +785,11 @@ public:
|
||||
/// <param name="other">The other collection to clone.</param>
|
||||
void Clone(const Dictionary& other)
|
||||
{
|
||||
// TODO: if both key and value are POD types then use raw memory copy for buckets
|
||||
Clear();
|
||||
SetCapacity(other.Capacity(), false);
|
||||
EnsureCapacity(other.Capacity(), false);
|
||||
for (Iterator i = other.Begin(); i != other.End(); ++i)
|
||||
Add(i);
|
||||
ASSERT(Count() == other.Count());
|
||||
ASSERT(Capacity() == other.Capacity());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1013,6 +1013,7 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
|
||||
const auto parent = Scripting::FindObject<Actor>(parentId);
|
||||
if (_parent != parent)
|
||||
{
|
||||
ScopeLock lock(Level::ScenesLock);
|
||||
if (IsDuringPlay())
|
||||
{
|
||||
SetParent(parent, false, false);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "Engine/Debug/Exceptions/JsonParseException.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"
|
||||
@@ -932,11 +933,9 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
|
||||
LOG(Error, "Invalid Data member.");
|
||||
return true;
|
||||
}
|
||||
int32 objectsCount = data.Size();
|
||||
|
||||
// Peek scene node value (it's the first actor serialized)
|
||||
auto& sceneValue = data[0];
|
||||
auto sceneId = JsonTools::GetGuid(sceneValue, "ID");
|
||||
auto sceneId = JsonTools::GetGuid(data[0], "ID");
|
||||
if (!sceneId.IsValid())
|
||||
{
|
||||
LOG(Error, "Invalid scene id.");
|
||||
@@ -957,59 +956,103 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
|
||||
auto scene = New<Scene>(ScriptingObjectSpawnParams(sceneId, Scene::TypeInitializer));
|
||||
scene->LoadTime = startTime;
|
||||
scene->RegisterObject();
|
||||
scene->Deserialize(sceneValue, modifier.Value);
|
||||
scene->Deserialize(data[0], modifier.Value);
|
||||
|
||||
// Fire event
|
||||
CallSceneEvent(SceneEventType::OnSceneLoading, scene, sceneId);
|
||||
|
||||
// Loaded scene objects list
|
||||
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
|
||||
const int32 objectsCount = (int32)data.Size();
|
||||
sceneObjects->Resize(objectsCount);
|
||||
sceneObjects->At(0) = scene;
|
||||
|
||||
// Spawn all scene objects
|
||||
SceneObjectsFactory::Context context(modifier.Value);
|
||||
context.Async = JobSystem::GetThreadsCount() > 1 && objectsCount > 10;
|
||||
{
|
||||
PROFILE_CPU_NAMED("Spawn");
|
||||
|
||||
// Spawn all scene objects
|
||||
for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
|
||||
SceneObject** objects = sceneObjects->Get();
|
||||
if (context.Async)
|
||||
{
|
||||
auto& stream = data[i];
|
||||
auto obj = SceneObjectsFactory::Spawn(context, stream);
|
||||
sceneObjects->At(i) = obj;
|
||||
if (obj)
|
||||
obj->RegisterObject();
|
||||
else
|
||||
SceneObjectsFactory::HandleObjectDeserializationError(stream);
|
||||
JobSystem::Execute([&](int32 i)
|
||||
{
|
||||
i++; // Start from 1. at index [0] was scene
|
||||
auto& stream = data[i];
|
||||
auto obj = SceneObjectsFactory::Spawn(context, stream);
|
||||
objects[i] = obj;
|
||||
if (obj)
|
||||
{
|
||||
obj->RegisterObject();
|
||||
#if USE_EDITOR
|
||||
// Auto-create C# objects for all actors in Editor during scene load when running in async (so main thread already has all of them)
|
||||
obj->CreateManaged();
|
||||
#endif
|
||||
}
|
||||
else
|
||||
SceneObjectsFactory::HandleObjectDeserializationError(stream);
|
||||
}, objectsCount - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
|
||||
{
|
||||
auto& stream = data[i];
|
||||
auto obj = SceneObjectsFactory::Spawn(context, stream);
|
||||
sceneObjects->At(i) = obj;
|
||||
if (obj)
|
||||
obj->RegisterObject();
|
||||
else
|
||||
SceneObjectsFactory::HandleObjectDeserializationError(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture prefab instances in a scene to restore any missing objects (eg. newly added objects to prefab that are missing in scene file)
|
||||
SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, data, modifier.Value);
|
||||
|
||||
SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData);
|
||||
|
||||
// TODO: resave and force sync scenes during game cooking so this step could be skipped in game
|
||||
SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData);
|
||||
|
||||
// /\ all above this has to be done on an any thread
|
||||
// \/ all below this has to be done on multiple threads at once
|
||||
|
||||
// Load all scene objects
|
||||
{
|
||||
PROFILE_CPU_NAMED("Deserialize");
|
||||
|
||||
// TODO: at this point we would probably spawn a few thread pool tasks which will load deserialize scene object but only if scene is big enough
|
||||
|
||||
// Load all scene objects
|
||||
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
|
||||
SceneObject** objects = sceneObjects->Get();
|
||||
for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
|
||||
bool wasAsync = context.Async;
|
||||
context.Async = false; // TODO: fix Actor's Scripts and Children order when loading objects data out of order via async jobs
|
||||
if (context.Async)
|
||||
{
|
||||
auto& objData = data[i];
|
||||
auto obj = objects[i];
|
||||
if (obj)
|
||||
SceneObjectsFactory::Deserialize(context, obj, objData);
|
||||
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)
|
||||
{
|
||||
i++; // Start from 1. at index [0] was scene
|
||||
auto obj = objects[i];
|
||||
if (obj)
|
||||
{
|
||||
auto& idMapping = Scripting::ObjectsLookupIdMapping.Get();
|
||||
idMapping = &context.GetModifier()->IdsMapping;
|
||||
SceneObjectsFactory::Deserialize(context, obj, data[i]);
|
||||
idMapping = nullptr;
|
||||
}
|
||||
}, objectsCount - 1);
|
||||
ScenesLock.Lock();
|
||||
}
|
||||
Scripting::ObjectsLookupIdMapping.Set(nullptr);
|
||||
else
|
||||
{
|
||||
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
|
||||
for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
|
||||
{
|
||||
auto& objData = data[i];
|
||||
auto obj = objects[i];
|
||||
if (obj)
|
||||
SceneObjectsFactory::Deserialize(context, obj, objData);
|
||||
}
|
||||
Scripting::ObjectsLookupIdMapping.Set(nullptr);
|
||||
}
|
||||
context.Async = wasAsync;
|
||||
}
|
||||
|
||||
// /\ all above this has to be done on multiple threads at once
|
||||
@@ -1031,7 +1074,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
|
||||
PROFILE_CPU_NAMED("Initialize");
|
||||
|
||||
SceneObject** objects = sceneObjects->Get();
|
||||
for (int32 i = 0; i < sceneObjects->Count(); i++)
|
||||
for (int32 i = 0; i < objectsCount; i++)
|
||||
{
|
||||
SceneObject* obj = objects[i];
|
||||
if (obj)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "Engine/Level/Actor.h"
|
||||
#include "Engine/Level/Prefabs/Prefab.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
#include "Engine/Core/Cache.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Scripting/Scripting.h"
|
||||
#include "Engine/Serialization/JsonTools.h"
|
||||
@@ -18,16 +19,49 @@ SceneObjectsFactory::Context::Context(ISerializeModifier* modifier)
|
||||
{
|
||||
}
|
||||
|
||||
void SceneObjectsFactory::Context::SetupIdsMapping(const SceneObject* obj)
|
||||
SceneObjectsFactory::Context::~Context()
|
||||
{
|
||||
if (Async)
|
||||
{
|
||||
Array<ISerializeModifier*, FixedAllocation<PLATFORM_THREADS_LIMIT>> modifiers;
|
||||
Modifiers.GetValues(modifiers);
|
||||
for (ISerializeModifier* e : modifiers)
|
||||
Cache::ISerializeModifier.Put(e);
|
||||
}
|
||||
}
|
||||
|
||||
ISerializeModifier* SceneObjectsFactory::Context::GetModifier()
|
||||
{
|
||||
ISerializeModifier* modifier = Modifier;
|
||||
if (Async)
|
||||
{
|
||||
// When using context in async then use one ISerializeModifier per-thread
|
||||
ISerializeModifier*& modifierThread = Modifiers.Get();
|
||||
if (!modifierThread)
|
||||
{
|
||||
modifierThread = Cache::ISerializeModifier.GetUnscoped();
|
||||
Modifiers.Set(modifierThread);
|
||||
Locker.Lock();
|
||||
modifierThread->EngineBuild = modifier->EngineBuild;
|
||||
modifierThread->CurrentInstance = modifier->CurrentInstance;
|
||||
modifierThread->IdsMapping = modifier->IdsMapping;
|
||||
Locker.Unlock();
|
||||
}
|
||||
modifier = modifierThread;
|
||||
}
|
||||
return modifier;
|
||||
}
|
||||
|
||||
void SceneObjectsFactory::Context::SetupIdsMapping(const SceneObject* obj, ISerializeModifier* modifier)
|
||||
{
|
||||
int32 instanceIndex;
|
||||
if (ObjectToInstance.TryGet(obj->GetID(), instanceIndex) && instanceIndex != CurrentInstance)
|
||||
if (ObjectToInstance.TryGet(obj->GetID(), instanceIndex) && instanceIndex != modifier->CurrentInstance)
|
||||
{
|
||||
// Apply the current prefab instance objects ids table to resolve references inside a prefab properly
|
||||
CurrentInstance = instanceIndex;
|
||||
modifier->CurrentInstance = instanceIndex;
|
||||
auto& instance = Instances[instanceIndex];
|
||||
for (auto& e : instance.IdsMapping)
|
||||
Modifier->IdsMapping[e.Key] = e.Value;
|
||||
modifier->IdsMapping[e.Key] = e.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +69,13 @@ SceneObject* SceneObjectsFactory::Spawn(Context& context, const ISerializable::D
|
||||
{
|
||||
// Get object id
|
||||
Guid id = JsonTools::GetGuid(stream, "ID");
|
||||
context.Modifier->IdsMapping.TryGet(id, id);
|
||||
ISerializeModifier* modifier = context.GetModifier();
|
||||
modifier->IdsMapping.TryGet(id, id);
|
||||
if (!id.IsValid())
|
||||
{
|
||||
LOG(Warning, "Invalid object id.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SceneObject* obj = nullptr;
|
||||
|
||||
// Check for prefab instance
|
||||
@@ -78,7 +112,7 @@ SceneObject* SceneObjectsFactory::Spawn(Context& context, const ISerializable::D
|
||||
}
|
||||
|
||||
// Map prefab object ID to the deserialized instance ID
|
||||
context.Modifier->IdsMapping[prefabObjectId] = id;
|
||||
modifier->IdsMapping[prefabObjectId] = id;
|
||||
|
||||
// Create prefab instance (recursive prefab loading to support nested prefabs)
|
||||
obj = Spawn(context, *prefabData);
|
||||
@@ -169,6 +203,7 @@ void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISeria
|
||||
#if ENABLE_ASSERTION
|
||||
CHECK(obj);
|
||||
#endif
|
||||
ISerializeModifier* modifier = context.GetModifier();
|
||||
|
||||
// Check for prefab instance
|
||||
Guid prefabObjectId;
|
||||
@@ -204,24 +239,16 @@ void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISeria
|
||||
}
|
||||
|
||||
// Deserialize prefab data (recursive prefab loading to support nested prefabs)
|
||||
const auto prevVersion = context.Modifier->EngineBuild;
|
||||
context.Modifier->EngineBuild = prefab->DataEngineBuild;
|
||||
const auto prevVersion = modifier->EngineBuild;
|
||||
modifier->EngineBuild = prefab->DataEngineBuild;
|
||||
Deserialize(context, obj, *(ISerializable::DeserializeStream*)prefabData);
|
||||
context.Modifier->EngineBuild = prevVersion;
|
||||
modifier->EngineBuild = prevVersion;
|
||||
}
|
||||
|
||||
int32 instanceIndex;
|
||||
if (context.ObjectToInstance.TryGet(obj->GetID(), instanceIndex) && instanceIndex != context.CurrentInstance)
|
||||
{
|
||||
// Apply the current prefab instance objects ids table to resolve references inside a prefab properly
|
||||
context.CurrentInstance = instanceIndex;
|
||||
auto& instance = context.Instances[instanceIndex];
|
||||
for (auto& e : instance.IdsMapping)
|
||||
context.Modifier->IdsMapping[e.Key] = e.Value;
|
||||
}
|
||||
context.SetupIdsMapping(obj, modifier);
|
||||
|
||||
// Load data
|
||||
obj->Deserialize(stream, context.Modifier);
|
||||
obj->Deserialize(stream, modifier);
|
||||
}
|
||||
|
||||
void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable::DeserializeStream& value)
|
||||
@@ -518,7 +545,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn
|
||||
LOG(Info, "Object {0} has invalid parent object {4} -> {5} (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", obj->GetSceneObjectId(), prefabObjectId, prefab->GetID(), prefab->GetPath(), parentPrefabObjectId, actualParentPrefabId);
|
||||
|
||||
// Map actual prefab object id to the current scene objects collection
|
||||
context.SetupIdsMapping(obj);
|
||||
context.SetupIdsMapping(obj, data.Modifier);
|
||||
data.Modifier->IdsMapping.TryGet(actualParentPrefabId, actualParentPrefabId);
|
||||
|
||||
// Find parent
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "SceneObject.h"
|
||||
#include "Engine/Core/Collections/Dictionary.h"
|
||||
#include "Engine/Platform/CriticalSection.h"
|
||||
#include "Engine/Threading/ThreadLocal.h"
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for scene objects creation and deserialization utilities.
|
||||
@@ -21,13 +23,17 @@ public:
|
||||
struct Context
|
||||
{
|
||||
ISerializeModifier* Modifier;
|
||||
int32 CurrentInstance = -1;
|
||||
bool Async = false;
|
||||
Array<PrefabInstance> Instances;
|
||||
Dictionary<Guid, int32> ObjectToInstance;
|
||||
CriticalSection Locker;
|
||||
ThreadLocal<ISerializeModifier*> Modifiers;
|
||||
|
||||
Context(ISerializeModifier* modifier);
|
||||
~Context();
|
||||
|
||||
void SetupIdsMapping(const SceneObject* obj);
|
||||
ISerializeModifier* GetModifier();
|
||||
void SetupIdsMapping(const SceneObject* obj, ISerializeModifier* modifier);
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -327,6 +327,7 @@ void Script::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier
|
||||
const auto parent = Scripting::FindObject<Actor>(parentId);
|
||||
if (_parent != parent)
|
||||
{
|
||||
ScopeLock lock(Level::ScenesLock);
|
||||
if (IsDuringPlay())
|
||||
{
|
||||
SetParent(parent, false);
|
||||
|
||||
@@ -18,6 +18,9 @@ public:
|
||||
/// </summary>
|
||||
uint32 EngineBuild = FLAXENGINE_VERSION_BUILD;
|
||||
|
||||
// Utility for scene deserialization to track currently mapped in Prefab Instance object IDs into IdsMapping.
|
||||
int32 CurrentInstance = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The object IDs mapping. Key is a serialized object id, value is mapped value to use.
|
||||
/// </summary>
|
||||
|
||||
@@ -228,6 +228,7 @@ int32 JobSystemThread::Run()
|
||||
|
||||
void JobSystem::Execute(const Function<void(int32)>& job, int32 jobCount)
|
||||
{
|
||||
#if JOB_SYSTEM_ENABLED
|
||||
// TODO: disable async if called on job thread? or maybe Wait should handle waiting in job thread to do the processing?
|
||||
if (jobCount > 1)
|
||||
{
|
||||
@@ -235,7 +236,8 @@ void JobSystem::Execute(const Function<void(int32)>& job, int32 jobCount)
|
||||
const int64 jobWaitHandle = Dispatch(job, jobCount);
|
||||
Wait(jobWaitHandle);
|
||||
}
|
||||
else if (jobCount > 0)
|
||||
else
|
||||
#endif
|
||||
{
|
||||
// Sync
|
||||
for (int32 i = 0; i < jobCount; i++)
|
||||
|
||||
@@ -65,10 +65,10 @@ public:
|
||||
template<typename AllocationType = HeapAllocation>
|
||||
void GetValues(Array<T, AllocationType>& result) const
|
||||
{
|
||||
result.EnsureCapacity(MaxThreads);
|
||||
for (int32 i = 0; i < MaxThreads; i++)
|
||||
{
|
||||
result.Add(_buckets[i].Value);
|
||||
if (Platform::AtomicRead((int64 volatile*)&_buckets[i].ThreadID) != 0)
|
||||
result.Add(_buckets[i].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user