Optimize scenes loading with Job System

This commit is contained in:
Wojtek Figat
2023-10-01 10:55:01 +02:00
parent f77198c7ca
commit b960600102
11 changed files with 162 additions and 69 deletions

View File

@@ -8,6 +8,7 @@ CollectionPoolCache<ISerializeModifier, Cache::ISerializeModifierClearCallback>
void Cache::ISerializeModifierClearCallback(::ISerializeModifier* obj)
{
obj->EngineBuild = FLAXENGINE_VERSION_BUILD;
obj->CurrentInstance = -1;
obj->IdsMapping.Clear();
}

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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)

View File

@@ -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

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -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++)

View File

@@ -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);
}
}