From d895789296afbc5f82027e02ab7ec59095098b12 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jun 2021 23:49:37 +0200 Subject: [PATCH] Add async particles updating via Task Graph --- .../Editor/Surface/ParticleEmitterSurface.cs | 4 +- Source/Engine/Animations/Graph/AnimGraph.cpp | 1 - Source/Engine/Particles/Particles.cpp | 461 +++++++++--------- Source/Engine/Particles/Particles.h | 14 +- 4 files changed, 256 insertions(+), 224 deletions(-) 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: ///