Add async particles updating via Task Graph

This commit is contained in:
Wojtek Figat
2021-06-15 23:49:37 +02:00
parent 1dfcfb8aa9
commit d895789296
4 changed files with 256 additions and 224 deletions

View File

@@ -17,12 +17,12 @@ namespace FlaxEditor.Surface
[HideInEditor]
public class ParticleEmitterSurface : VisjectSurface
{
internal Particles.ParticleEmitterNode _rootNode;
internal FlaxEditor.Surface.Archetypes.Particles.ParticleEmitterNode _rootNode;
/// <summary>
/// Gets the root node of the emitter graph.
/// </summary>
public Particles.ParticleEmitterNode RootNode => _rootNode;
public FlaxEditor.Surface.Archetypes.Particles.ParticleEmitterNode RootNode => _rootNode;
/// <inheritdoc />
public ParticleEmitterSurface(IVisjectSurfaceOwner owner, Action onSave, FlaxEditor.Undo undo)

View File

@@ -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<AnimGraphContext, 64> AnimGraphExecutor::Context;

View File

@@ -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<ParticleEmitter*, Array<EmitterCache>> Pool;
HashSet<ParticleEffect*> UpdateList(256);
Array<ParticleEffect*> UpdateList;
#if COMPILE_WITH_GPU_PARTICLES
HashSet<ParticleEffect*> GpuUpdateList(256);
CriticalSection GpuUpdateListLocker;
Array<ParticleEffect*> 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<RenderTask>();
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<ParticlesSystem>();
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<void(int32)> job;
job.Bind<ParticlesSystem, &ParticlesSystem::Job>(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<RenderTask>();
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();
}

View File

@@ -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;
/// <summary>
/// The particles service used for simulation and emitters data pooling.
/// The particles simulation service.
/// </summary>
class FLAXENGINE_API Particles
API_CLASS(Static) class FLAXENGINE_API Particles
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(Particles);
/// <summary>
/// The system for Particles update.
/// </summary>
API_FIELD(ReadOnly) static TaskGraphSystem* System;
public:
/// <summary>