From 526edb83de5a427daf787025e5334dead5ecd2cf Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 7 Dec 2023 15:21:03 +0100 Subject: [PATCH] Add `Async` to anim events (`false` by default) to delay events execution into main thread and prevent multi-threading issues by default #2033 --- Source/Engine/Animations/AnimEvent.h | 5 ++ Source/Engine/Animations/Animations.cpp | 1 + Source/Engine/Animations/Graph/AnimGraph.cpp | 73 +++++++++++++------ Source/Engine/Animations/Graph/AnimGraph.h | 33 ++++++++- .../Animations/Graph/AnimGroup.Animation.cpp | 28 +++---- 5 files changed, 103 insertions(+), 37 deletions(-) diff --git a/Source/Engine/Animations/AnimEvent.h b/Source/Engine/Animations/AnimEvent.h index 4ed7eab48..6cf54459f 100644 --- a/Source/Engine/Animations/AnimEvent.h +++ b/Source/Engine/Animations/AnimEvent.h @@ -17,6 +17,11 @@ API_CLASS(Abstract) class FLAXENGINE_API AnimEvent : public SerializableScriptin { DECLARE_SCRIPTING_TYPE(AnimEvent); + /// + /// Indicates whether the event can be executed in async from a thread that updates the animated model. Otherwise, event execution will be delayed until the sync point of the animated model and called from the main thread. Async events need to precisely handle data access, especially when it comes to editing scene objects with multi-threading. + /// + API_FIELD(Attributes="HideInEditor, NoSerialize") bool Async = false; + #if USE_EDITOR /// /// Event display color in the Editor. diff --git a/Source/Engine/Animations/Animations.cpp b/Source/Engine/Animations/Animations.cpp index 6599bebac..e571af162 100644 --- a/Source/Engine/Animations/Animations.cpp +++ b/Source/Engine/Animations/Animations.cpp @@ -146,6 +146,7 @@ void AnimationsSystem::PostExecute(TaskGraph* graph) auto animatedModel = AnimationManagerInstance.UpdateList[index]; if (CanUpdateModel(animatedModel)) { + animatedModel->GraphInstance.InvokeAnimEvents(); animatedModel->OnAnimationUpdated_Sync(); } } diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index 71c9534e0..5aa720d31 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -38,22 +38,16 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32 void AnimGraphInstanceData::Clear() { - Version = 0; - LastUpdateTime = -1; - CurrentFrame = 0; - RootTransform = Transform::Identity; - RootMotion = Transform::Identity; + ClearState(); Parameters.Resize(0); - State.Resize(0); - NodesPose.Resize(0); - Slots.Resize(0); - for (const auto& e : Events) - ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f); - Events.Resize(0); } void AnimGraphInstanceData::ClearState() { + for (const auto& e : ActiveEvents) + OutgoingEvents.Add(e.End((AnimatedModel*)Object)); + ActiveEvents.Clear(); + InvokeAnimEvents(); Version = 0; LastUpdateTime = -1; CurrentFrame = 0; @@ -62,9 +56,6 @@ void AnimGraphInstanceData::ClearState() State.Resize(0); NodesPose.Resize(0); Slots.Clear(); - for (const auto& e : Events) - ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f); - Events.Clear(); } void AnimGraphInstanceData::Invalidate() @@ -73,6 +64,43 @@ void AnimGraphInstanceData::Invalidate() CurrentFrame = 0; } +void AnimGraphInstanceData::InvokeAnimEvents() +{ + const bool all = IsInMainThread(); + for (int32 i = 0; i < OutgoingEvents.Count(); i++) + { + const OutgoingEvent e = OutgoingEvents[i]; + if (all || e.Instance->Async) + { + OutgoingEvents.RemoveAtKeepOrder(i); + switch (e.Type) + { + case OutgoingEvent::OnEvent: + e.Instance->OnEvent(e.Actor, e.Anim, e.Time, e.DeltaTime); + break; + case OutgoingEvent::OnBegin: + ((AnimContinuousEvent*)e.Instance)->OnBegin(e.Actor, e.Anim, e.Time, e.DeltaTime); + break; + case OutgoingEvent::OnEnd: + ((AnimContinuousEvent*)e.Instance)->OnEnd(e.Actor, e.Anim, e.Time, e.DeltaTime); + break; + } + } + } +} + +AnimGraphInstanceData::OutgoingEvent AnimGraphInstanceData::ActiveEvent::End(AnimatedModel* actor) const +{ + OutgoingEvent out; + out.Instance = Instance; + out.Actor = actor; + out.Anim = Anim; + out.Time = 0.0f; + out.DeltaTime = 0.0f; + out.Type = OutgoingEvent::OnEnd; + return out; +} + AnimGraphImpulse* AnimGraphNode::GetNodes(AnimGraphExecutor* executor) { auto& context = AnimGraphExecutor::Context.Get(); @@ -208,7 +236,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) // Initialize buckets ResetBuckets(context, &_graph); } - for (auto& e : data.Events) + for (auto& e : data.ActiveEvents) e.Hit = false; // Init empty nodes data @@ -240,16 +268,17 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) if (animResult == nullptr) animResult = GetEmptyNodes(); } - if (data.Events.Count() != 0) + if (data.ActiveEvents.Count() != 0) { ANIM_GRAPH_PROFILE_EVENT("Events"); - for (int32 i = data.Events.Count() - 1; i >= 0; i--) + for (int32 i = data.ActiveEvents.Count() - 1; i >= 0; i--) { - const auto& e = data.Events[i]; + const auto& e = data.ActiveEvents[i]; if (!e.Hit) { - ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)context.Data->Object, e.Anim, 0.0f, 0.0f); - data.Events.RemoveAt(i); + // Remove active event that was not hit during this frame (eg. animation using it was not used in blending) + data.OutgoingEvents.Add(e.End((AnimatedModel*)context.Data->Object)); + data.ActiveEvents.RemoveAt(i); } } } @@ -284,7 +313,6 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, i); targetNodes[i] = node; } - } } @@ -319,6 +347,9 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) data.RootMotion = animResult->RootMotion; } + // Invoke any async anim events + context.Data->InvokeAnimEvents(); + // Cleanup context.Data = nullptr; } diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index 41edb72f3..afaa4441b 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -23,6 +23,9 @@ class AnimSubGraph; class AnimGraphBase; class AnimGraphNode; class AnimGraphExecutor; +class AnimatedModel; +class AnimEvent; +class AnimContinuousEvent; class SkinnedModel; class SkeletonData; @@ -349,16 +352,40 @@ public: /// void Invalidate(); + /// + /// Invokes any outgoing AnimEvent and AnimContinuousEvent collected during the last animation update. When called from non-main thread only Async events will be invoked. + /// + void InvokeAnimEvents(); + private: - struct Event + struct OutgoingEvent { + enum Types + { + OnEvent, + OnBegin, + OnEnd, + }; + AnimEvent* Instance; + AnimatedModel* Actor; + Animation* Anim; + float Time, DeltaTime; + Types Type; + }; + + struct ActiveEvent + { + AnimContinuousEvent* Instance; Animation* Anim; AnimGraphNode* Node; bool Hit; + + OutgoingEvent End(AnimatedModel* actor) const; }; - Array> Events; + Array> ActiveEvents; + Array> OutgoingEvents; }; /// @@ -441,7 +468,7 @@ public: /// The invalid transition valid used in Transitions to indicate invalid transition linkage. /// const static uint16 InvalidTransitionIndex = MAX_uint16; - + /// /// The outgoing transitions from this state to the other states. Each array item contains index of the transition data from the state node graph transitions cache. Value InvalidTransitionIndex is used for last transition to indicate the transitions amount. /// diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 6f0d7661b..674da5839 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -100,48 +100,50 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float if (!k.Value.Instance) continue; const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f; +#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type }) if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration) { int32 stateIndex = -1; if (duration > 1) { // Begin for continuous event - for (stateIndex = 0; stateIndex < context.Data->Events.Count(); stateIndex++) + for (stateIndex = 0; stateIndex < context.Data->ActiveEvents.Count(); stateIndex++) { - const auto& e = context.Data->Events[stateIndex]; + const auto& e = context.Data->ActiveEvents[stateIndex]; if (e.Instance == k.Value.Instance && e.Node == node) break; } - if (stateIndex == context.Data->Events.Count()) + if (stateIndex == context.Data->ActiveEvents.Count()) { - auto& e = context.Data->Events.AddOne(); - e.Instance = k.Value.Instance; + ASSERT(k.Value.Instance->Is()); + auto& e = context.Data->ActiveEvents.AddOne(); + e.Instance = (AnimContinuousEvent*)k.Value.Instance; e.Anim = anim; e.Node = node; - ASSERT(k.Value.Instance->Is()); - ((AnimContinuousEvent*)k.Value.Instance)->OnBegin((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime); + ADD_OUTGOING_EVENT(OnBegin); } } // Event - k.Value.Instance->OnEvent((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime); + ADD_OUTGOING_EVENT(OnEvent); if (stateIndex != -1) - context.Data->Events[stateIndex].Hit = true; + context.Data->ActiveEvents[stateIndex].Hit = true; } else if (duration > 1) { // End for continuous event - for (int32 i = 0; i < context.Data->Events.Count(); i++) + for (int32 i = 0; i < context.Data->ActiveEvents.Count(); i++) { - const auto& e = context.Data->Events[i]; + const auto& e = context.Data->ActiveEvents[i]; if (e.Instance == k.Value.Instance && e.Node == node) { - ((AnimContinuousEvent*)k.Value.Instance)->OnEnd((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime); - context.Data->Events.RemoveAt(i); + ADD_OUTGOING_EVENT(OnEnd); + context.Data->ActiveEvents.RemoveAt(i); break; } } } +#undef ADD_OUTGOING_EVENT } } }