diff --git a/Source/Editor/Surface/ParticleEmitterSurface.cs b/Source/Editor/Surface/ParticleEmitterSurface.cs
index 4368569a1..905d41882 100644
--- a/Source/Editor/Surface/ParticleEmitterSurface.cs
+++ b/Source/Editor/Surface/ParticleEmitterSurface.cs
@@ -17,12 +17,12 @@ namespace FlaxEditor.Surface
[HideInEditor]
public class ParticleEmitterSurface : VisjectSurface
{
- internal Particles.ParticleEmitterNode _rootNode;
+ internal FlaxEditor.Surface.Archetypes.Particles.ParticleEmitterNode _rootNode;
///
/// Gets the root node of the emitter graph.
///
- public Particles.ParticleEmitterNode RootNode => _rootNode;
+ public FlaxEditor.Surface.Archetypes.Particles.ParticleEmitterNode RootNode => _rootNode;
///
public ParticleEmitterSurface(IVisjectSurfaceOwner owner, Action onSave, FlaxEditor.Undo undo)
diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp
index 6d95cbdfc..e726fbacc 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.cpp
@@ -5,7 +5,6 @@
#include "Engine/Content/Assets/SkinnedModel.h"
#include "Engine/Graphics/Models/SkeletonData.h"
#include "Engine/Scripting/Scripting.h"
-#include "Engine/Engine/Time.h"
ThreadLocal AnimGraphExecutor::Context;
diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp
index 17c1e3da4..c3f72270c 100644
--- a/Source/Engine/Particles/Particles.cpp
+++ b/Source/Engine/Particles/Particles.cpp
@@ -1,11 +1,13 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#include "Particles.h"
+#include "ParticleEffect.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Core/Collections/HashSet.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Engine/Time.h"
+#include "Engine/Engine/Engine.h"
#include "Engine/Graphics/GPUBuffer.h"
#include "Engine/Graphics/GPUPipelineStatePermutations.h"
#include "Engine/Graphics/RenderTask.h"
@@ -13,7 +15,7 @@
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Renderer/DrawCall.h"
#include "Engine/Renderer/RenderList.h"
-#include "ParticleEffect.h"
+#include "Engine/Threading/TaskGraph.h"
#if COMPILE_WITH_GPU_PARTICLES
#include "Engine/Content/Assets/Shader.h"
#include "Engine/Profiler/ProfilerGPU.h"
@@ -46,10 +48,8 @@ public:
{
if (VB)
return false;
-
VB = GPUDevice::Instance->CreateBuffer(TEXT("SpriteParticleRenderer,VB"));
IB = GPUDevice::Instance->CreateBuffer(TEXT("SpriteParticleRenderer.IB"));
-
static SpriteParticleVertex vertexBuffer[] =
{
{ -0.5f, -0.5f, 0.0f, 0.0f },
@@ -57,20 +57,16 @@ public:
{ +0.5f, +0.5f, 1.0f, 1.0f },
{ -0.5f, +0.5f, 0.0f, 1.0f },
};
-
static uint16 indexBuffer[] =
{
0,
1,
2,
-
0,
2,
3,
};
-
- return VB->Init(GPUBufferDescription::Vertex(sizeof(SpriteParticleVertex), VertexCount, vertexBuffer)) ||
- IB->Init(GPUBufferDescription::Index(sizeof(uint16), IndexCount, indexBuffer));
+ return VB->Init(GPUBufferDescription::Vertex(sizeof(SpriteParticleVertex), VertexCount, vertexBuffer)) || IB->Init(GPUBufferDescription::Index(sizeof(uint16), IndexCount, indexBuffer));
}
void Dispose()
@@ -103,15 +99,17 @@ namespace ParticleManagerImpl
{
CriticalSection PoolLocker;
Dictionary> Pool;
- HashSet UpdateList(256);
+ Array UpdateList;
#if COMPILE_WITH_GPU_PARTICLES
- HashSet GpuUpdateList(256);
+ CriticalSection GpuUpdateListLocker;
+ Array GpuUpdateList;
RenderTask* GpuRenderTask = nullptr;
#endif
}
using namespace ParticleManagerImpl;
+TaskGraphSystem* Particles::System = nullptr;
bool Particles::EnableParticleBufferPooling = true;
float Particles::ParticleBufferRecycleTimeout = 10.0f;
@@ -149,14 +147,24 @@ public:
{
}
- void Update() override;
+ bool Init() override;
void Dispose() override;
};
+class ParticlesSystem : public TaskGraphSystem
+{
+public:
+ float DeltaTime, UnscaledDeltaTime, Time, UnscaledTime;
+ void Job(int32 index);
+ void Execute(TaskGraph* graph) override;
+ void PostExecute(TaskGraph* graph) override;
+};
+
ParticleManagerService ParticleManagerServiceInstance;
void Particles::UpdateEffect(ParticleEffect* effect)
{
+ ASSERT_LOW_LAYER(!UpdateList.Contains(effect));
UpdateList.Add(effect);
}
@@ -1027,14 +1035,14 @@ void Particles::DrawParticles(RenderContext& renderContext, ParticleEffect* effe
void UpdateGPU(RenderTask* task, GPUContext* context)
{
+ ScopeLock lock(GpuUpdateListLocker);
if (GpuUpdateList.IsEmpty())
return;
PROFILE_GPU("GPU Particles");
- for (auto i = GpuUpdateList.Begin(); i.IsNotEnd(); ++i)
+ for (ParticleEffect* effect : GpuUpdateList)
{
- ParticleEffect* effect = i->Item;
auto& instance = effect->Instance;
const auto particleSystem = effect->ParticleSystem.Get();
if (!particleSystem || !particleSystem->IsLoaded())
@@ -1155,218 +1163,22 @@ void Particles::OnEmitterUnload(ParticleEmitter* emitter)
PoolLocker.Unlock();
#if COMPILE_WITH_GPU_PARTICLES
- for (auto i = GpuUpdateList.Begin(); i.IsNotEnd(); ++i)
+ GpuUpdateListLocker.Lock();
+ for (int32 i = GpuUpdateList.Count() - 1; i >= 0; i--)
{
- if (i->Item->Instance.ContainsEmitter(emitter))
- GpuUpdateList.Remove(i);
+ if (GpuUpdateList[i]->Instance.ContainsEmitter(emitter))
+ GpuUpdateList.RemoveAt(i);
}
+ GpuUpdateListLocker.Unlock();
#endif
}
-void ParticleManagerService::Update()
+bool ParticleManagerService::Init()
{
- PROFILE_CPU_NAMED("Particles");
-
- // TODO: implement the thread jobs pipeline to run set of tasks at once (use it for multi threaded rendering and animations evaluation and CPU particles simulation)
-
- const auto timeSeconds = Platform::GetTimeSeconds();
- const auto& tickData = Time::Update;
- const float deltaTimeUnscaled = tickData.UnscaledDeltaTime.GetTotalSeconds();
- const float timeUnscaled = tickData.UnscaledTime.GetTotalSeconds();
- const float deltaTime = tickData.DeltaTime.GetTotalSeconds();
- const float time = tickData.Time.GetTotalSeconds();
-
- // Update particle effects
- for (auto i = UpdateList.Begin(); i.IsNotEnd(); ++i)
- {
- ParticleEffect* effect = i->Item;
- auto& instance = effect->Instance;
- const auto particleSystem = effect->ParticleSystem.Get();
- if (!particleSystem || !particleSystem->IsLoaded())
- continue;
- bool anyEmitterNotReady = false;
- for (int32 j = 0; j < particleSystem->Tracks.Count(); j++)
- {
- const auto& track = particleSystem->Tracks[j];
- if (track.Type != ParticleSystem::Track::Types::Emitter || track.Disabled)
- continue;
- auto emitter = particleSystem->Emitters[track.AsEmitter.Index].Get();
- if (!emitter || !emitter->IsLoaded())
- {
- anyEmitterNotReady = true;
- break;
- }
- }
- if (anyEmitterNotReady)
- continue;
-
-#if USE_EDITOR
- // Lock in editor only (more reloads during asset live editing)
- ScopeLock lock(particleSystem->Locker);
-#endif
-
- // Prepare instance data
- instance.Sync(particleSystem);
-
- bool updateBounds = false;
- bool updateGpu = false;
-
- // Simulation delta time can be based on a time since last update or the current delta time
- float dt = effect->UseTimeScale ? deltaTime : deltaTimeUnscaled;
- float t = effect->UseTimeScale ? time : timeUnscaled;
-#if USE_EDITOR
- if (!Editor::IsPlayMode)
- {
- dt = deltaTimeUnscaled;
- t = timeUnscaled;
- }
-#endif
- const float lastUpdateTime = instance.LastUpdateTime;
- if (lastUpdateTime > 0 && t > lastUpdateTime)
- {
- dt = t - lastUpdateTime;
- }
- else if (lastUpdateTime < 0)
- {
- // Update bounds after first system update
- updateBounds = true;
- }
- // TODO: if using fixed timestep quantize the dt and accumulate remaining part for the next update?
- if (dt <= 1.0f / 240.0f)
- continue;
- dt *= effect->SimulationSpeed;
- instance.Time += dt;
- const float fps = particleSystem->FramesPerSecond;
- const float duration = particleSystem->DurationFrames / fps;
- if (instance.Time > duration)
- {
- if (effect->IsLooping)
- {
- // Loop
- // TODO: accumulate (duration - instance.Time) into next update dt
- instance.Time = 0;
- for (int32 j = 0; j < instance.Emitters.Count(); j++)
- {
- auto& e = instance.Emitters[j];
- e.Time = 0;
- for (auto& s : e.SpawnModulesData)
- {
- s.NextSpawnTime = 0.0f;
- }
- }
- }
- else
- {
- // End
- instance.Time = duration;
- for (auto& emitterInstance : instance.Emitters)
- {
- if (emitterInstance.Buffer)
- {
- Particles::RecycleParticleBuffer(emitterInstance.Buffer);
- emitterInstance.Buffer = nullptr;
- }
- }
- continue;
- }
- }
- instance.LastUpdateTime = t;
-
- // Update all emitter tracks
- for (int32 j = 0; j < particleSystem->Tracks.Count(); j++)
- {
- const auto& track = particleSystem->Tracks[j];
- if (track.Type != ParticleSystem::Track::Types::Emitter || track.Disabled)
- continue;
- auto emitter = particleSystem->Emitters[track.AsEmitter.Index].Get();
- auto& data = instance.Emitters[track.AsEmitter.Index];
- ASSERT(emitter && emitter->IsLoaded());
- ASSERT(emitter->Capacity != 0 && emitter->Graph.Layout.Size != 0);
-
- // Calculate new time position
- const float startTime = track.AsEmitter.StartFrame / fps;
- const float durationTime = track.AsEmitter.DurationFrames / fps;
- const bool canSpawn = startTime <= instance.Time && instance.Time <= startTime + durationTime;
-
- // Update instance data
- data.Sync(effect->Instance, particleSystem, track.AsEmitter.Index);
- if (!data.Buffer)
- {
- data.Buffer = Particles::AcquireParticleBuffer(emitter);
- }
- data.Time += dt;
-
- // Update particles simulation
- switch (emitter->SimulationMode)
- {
- case ParticlesSimulationMode::CPU:
- emitter->GraphExecutorCPU.Update(emitter, effect, data, dt, canSpawn);
- updateBounds |= emitter->UseAutoBounds;
- break;
-#if COMPILE_WITH_GPU_PARTICLES
- case ParticlesSimulationMode::GPU:
- emitter->GPU.Update(emitter, effect, data, dt, canSpawn);
- updateGpu = true;
- break;
-#endif
- default:
- CRASH;
- break;
- }
- }
-
- // Update bounds if any of the emitters uses auto-bounds
- if (updateBounds)
- {
- effect->UpdateBounds();
- }
-
-#if COMPILE_WITH_GPU_PARTICLES
- // Register for GPU update
- if (updateGpu)
- {
- GpuUpdateList.Add(effect);
- }
-#endif
- }
- UpdateList.Clear();
-
-#if COMPILE_WITH_GPU_PARTICLES
- // Create GPU render task if missing but required
- if (GpuUpdateList.HasItems() && !GpuRenderTask)
- {
- GpuRenderTask = New();
- GpuRenderTask->Order = -10000000;
- GpuRenderTask->Render.Bind(UpdateGPU);
- ScopeLock lock(RenderTask::TasksLocker);
- RenderTask::Tasks.Add(GpuRenderTask);
- }
- else if (GpuRenderTask)
- {
- ScopeLock lock(RenderTask::TasksLocker);
- GpuRenderTask->Enabled = GpuUpdateList.HasItems();
- }
-#endif
-
- // Recycle buffers
- PoolLocker.Lock();
- for (auto i = Pool.Begin(); i.IsNotEnd(); ++i)
- {
- auto& entries = i->Value;
- for (int32 j = 0; j < entries.Count(); j++)
- {
- auto& e = entries[j];
- if (timeSeconds - e.LastTimeUsed >= Particles::ParticleBufferRecycleTimeout)
- {
- Delete(e.Buffer);
- entries.RemoveAt(j--);
- }
- }
-
- if (entries.IsEmpty())
- Pool.Remove(i);
- }
- PoolLocker.Unlock();
+ Particles::System = New();
+ Particles::System->Order = 10000;
+ Engine::UpdateGraph->AddSystem(Particles::System);
+ return false;
}
void ParticleManagerService::Dispose()
@@ -1401,4 +1213,215 @@ void ParticleManagerService::Dispose()
PoolLocker.Unlock();
SpriteRenderer.Dispose();
+ SAFE_DELETE(Particles::System);
+}
+
+void ParticlesSystem::Job(int32 index)
+{
+ PROFILE_CPU_NAMED("Particles.Job");
+ auto effect = UpdateList[index];
+ auto& instance = effect->Instance;
+ const auto particleSystem = effect->ParticleSystem.Get();
+ if (!particleSystem || !particleSystem->IsLoaded())
+ return;
+ bool anyEmitterNotReady = false;
+ for (int32 j = 0; j < particleSystem->Tracks.Count(); j++)
+ {
+ const auto& track = particleSystem->Tracks[j];
+ if (track.Type != ParticleSystem::Track::Types::Emitter || track.Disabled)
+ continue;
+ auto emitter = particleSystem->Emitters[track.AsEmitter.Index].Get();
+ if (!emitter || !emitter->IsLoaded())
+ {
+ anyEmitterNotReady = true;
+ break;
+ }
+ }
+ if (anyEmitterNotReady)
+ return;
+
+ // Prepare instance data
+ instance.Sync(particleSystem);
+
+ bool updateBounds = false;
+ bool updateGpu = false;
+
+ // Simulation delta time can be based on a time since last update or the current delta time
+ bool useTimeScale = effect->UseTimeScale;
+#if USE_EDITOR
+ if (!Editor::IsPlayMode)
+ useTimeScale = false;
+#endif
+ float dt = useTimeScale ? DeltaTime : UnscaledDeltaTime;
+ float t = useTimeScale ? Time : UnscaledTime;
+ const float lastUpdateTime = instance.LastUpdateTime;
+ if (lastUpdateTime > 0 && t > lastUpdateTime)
+ {
+ dt = t - lastUpdateTime;
+ }
+ else if (lastUpdateTime < 0)
+ {
+ // Update bounds after first system update
+ updateBounds = true;
+ }
+ // TODO: if using fixed timestep quantize the dt and accumulate remaining part for the next update?
+ if (dt <= 1.0f / 240.0f)
+ return;
+ dt *= effect->SimulationSpeed;
+ instance.Time += dt;
+ const float fps = particleSystem->FramesPerSecond;
+ const float duration = (float)particleSystem->DurationFrames / fps;
+ if (instance.Time > duration)
+ {
+ if (effect->IsLooping)
+ {
+ // Loop
+ // TODO: accumulate (duration - instance.Time) into next update dt
+ instance.Time = 0;
+ for (int32 j = 0; j < instance.Emitters.Count(); j++)
+ {
+ auto& e = instance.Emitters[j];
+ e.Time = 0;
+ for (auto& s : e.SpawnModulesData)
+ {
+ s.NextSpawnTime = 0.0f;
+ }
+ }
+ }
+ else
+ {
+ // End
+ instance.Time = duration;
+ for (auto& emitterInstance : instance.Emitters)
+ {
+ if (emitterInstance.Buffer)
+ {
+ Particles::RecycleParticleBuffer(emitterInstance.Buffer);
+ emitterInstance.Buffer = nullptr;
+ }
+ }
+ return;
+ }
+ }
+ instance.LastUpdateTime = t;
+
+ // Update all emitter tracks
+ for (int32 j = 0; j < particleSystem->Tracks.Count(); j++)
+ {
+ const auto& track = particleSystem->Tracks[j];
+ if (track.Type != ParticleSystem::Track::Types::Emitter || track.Disabled)
+ continue;
+ auto emitter = particleSystem->Emitters[track.AsEmitter.Index].Get();
+ auto& data = instance.Emitters[track.AsEmitter.Index];
+ ASSERT(emitter && emitter->IsLoaded());
+ ASSERT(emitter->Capacity != 0 && emitter->Graph.Layout.Size != 0);
+
+ // Calculate new time position
+ const float startTime = (float)track.AsEmitter.StartFrame / fps;
+ const float durationTime = (float)track.AsEmitter.DurationFrames / fps;
+ const bool canSpawn = startTime <= instance.Time && instance.Time <= startTime + durationTime;
+
+ // Update instance data
+ data.Sync(effect->Instance, particleSystem, track.AsEmitter.Index);
+ if (!data.Buffer)
+ {
+ data.Buffer = Particles::AcquireParticleBuffer(emitter);
+ }
+ data.Time += dt;
+
+ // Update particles simulation
+ switch (emitter->SimulationMode)
+ {
+ case ParticlesSimulationMode::CPU:
+ emitter->GraphExecutorCPU.Update(emitter, effect, data, dt, canSpawn);
+ updateBounds |= emitter->UseAutoBounds;
+ break;
+#if COMPILE_WITH_GPU_PARTICLES
+ case ParticlesSimulationMode::GPU:
+ emitter->GPU.Update(emitter, effect, data, dt, canSpawn);
+ updateGpu = true;
+ break;
+#endif
+ default:
+ break;
+ }
+ }
+
+ // Update bounds if any of the emitters uses auto-bounds
+ if (updateBounds)
+ {
+ effect->UpdateBounds();
+ }
+
+#if COMPILE_WITH_GPU_PARTICLES
+ // Register for GPU update
+ if (updateGpu)
+ {
+ ScopeLock lock(GpuUpdateListLocker);
+ GpuUpdateList.Add(effect);
+ }
+#endif
+}
+
+void ParticlesSystem::Execute(TaskGraph* graph)
+{
+ if (UpdateList.Count() == 0)
+ return;
+
+ // Setup data for async update
+ const auto& tickData = Time::Update;
+ DeltaTime = tickData.DeltaTime.GetTotalSeconds();
+ UnscaledDeltaTime = tickData.UnscaledDeltaTime.GetTotalSeconds();
+ Time = tickData.Time.GetTotalSeconds();
+ UnscaledTime = tickData.UnscaledTime.GetTotalSeconds();
+
+ // Schedule work to update all particles in async
+ Function job;
+ job.Bind(this);
+ graph->DispatchJob(job, UpdateList.Count());
+}
+
+void ParticlesSystem::PostExecute(TaskGraph* graph)
+{
+ PROFILE_CPU_NAMED("Particles.PostExecute");
+
+ UpdateList.Clear();
+
+#if COMPILE_WITH_GPU_PARTICLES
+ // Create GPU render task if missing but required
+ if (GpuUpdateList.HasItems() && !GpuRenderTask)
+ {
+ GpuRenderTask = New();
+ GpuRenderTask->Order = -10000000;
+ GpuRenderTask->Render.Bind(UpdateGPU);
+ ScopeLock lock(RenderTask::TasksLocker);
+ RenderTask::Tasks.Add(GpuRenderTask);
+ }
+ else if (GpuRenderTask)
+ {
+ ScopeLock lock(RenderTask::TasksLocker);
+ GpuRenderTask->Enabled = GpuUpdateList.HasItems();
+ }
+#endif
+
+ // Recycle buffers
+ const auto timeSeconds = Platform::GetTimeSeconds();
+ PoolLocker.Lock();
+ for (auto i = Pool.Begin(); i.IsNotEnd(); ++i)
+ {
+ auto& entries = i->Value;
+ for (int32 j = 0; j < entries.Count(); j++)
+ {
+ auto& e = entries[j];
+ if (timeSeconds - e.LastTimeUsed >= Particles::ParticleBufferRecycleTimeout)
+ {
+ Delete(e.Buffer);
+ entries.RemoveAt(j--);
+ }
+ }
+
+ if (entries.IsEmpty())
+ Pool.Remove(i);
+ }
+ PoolLocker.Unlock();
}
diff --git a/Source/Engine/Particles/Particles.h b/Source/Engine/Particles/Particles.h
index ed66d7bb4..276eeb5af 100644
--- a/Source/Engine/Particles/Particles.h
+++ b/Source/Engine/Particles/Particles.h
@@ -2,6 +2,9 @@
#pragma once
+#include "Engine/Scripting/ScriptingType.h"
+
+class TaskGraphSystem;
struct RenderContext;
struct RenderView;
class ParticleEmitter;
@@ -13,10 +16,17 @@ class SceneRenderTask;
class Actor;
///
-/// The particles service used for simulation and emitters data pooling.
+/// The particles simulation service.
///
-class FLAXENGINE_API Particles
+API_CLASS(Static) class FLAXENGINE_API Particles
{
+DECLARE_SCRIPTING_TYPE_NO_SPAWN(Particles);
+
+ ///
+ /// The system for Particles update.
+ ///
+ API_FIELD(ReadOnly) static TaskGraphSystem* System;
+
public:
///