perf(runtime): shrink StateMachineLayerInstance and LinearAnimationInstance (#13894) 51f34f0bfb Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head index 56f4161..d305811 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -ec359aee6a0dd15454165be16cad99ddcf2ecbd1 +51f34f0bfb63944724485669e6cae7d8d16ebc8e
diff --git a/include/rive/animation/blend_state_instance.hpp b/include/rive/animation/blend_state_instance.hpp index dcaa1ba..cb7ca18 100644 --- a/include/rive/animation/blend_state_instance.hpp +++ b/include/rive/animation/blend_state_instance.hpp
@@ -59,15 +59,6 @@ BlendStateAnimationInstance<T>(static_cast<T*>(blendAnimation), instance)); } - if ((static_cast<LayerStateFlags>(blendState->flags()) & - LayerStateFlags::Reset) == LayerStateFlags::Reset) - { - auto animations = std::vector<const LinearAnimation*>(); - for (auto blendAnimation : blendState->animations()) - { - animations.push_back(blendAnimation->animation()); - } - } } bool keepGoing() const override { return m_KeepGoing; }
diff --git a/include/rive/animation/linear_animation_instance.hpp b/include/rive/animation/linear_animation_instance.hpp index 7b35d37..e928fa6 100644 --- a/include/rive/animation/linear_animation_instance.hpp +++ b/include/rive/animation/linear_animation_instance.hpp
@@ -18,6 +18,7 @@ class DataBind; class KeyFrame; class BindableProperty; +struct LAIBindingExtras; class LinearAnimationInstance : public Scene, public NestedEventNotifier { @@ -150,49 +151,22 @@ // float because it gets multiplied with other floats float m_direction; - bool m_didLoop; + // Initialized here because the primary ctor does not set it; didLoop() + // is readable before the first advance(). + bool m_didLoop = false; int m_loopValue = -1; - // Lazy outer pointer => the common case (no scripted interpolators) pays - // one nullptr check on the apply hot path. Inner unique_ptr destroys the - // cloned ScriptedInterpolator (and its Lua ref) when this LAI is - // destroyed. `mutable` so the cache populates from the const apply(). - // Intentionally not copied by the copy ctor — a copied LAI starts with - // a fresh empty cache. - mutable std::unique_ptr< - std::unordered_map<const InterpolatingKeyFrame*, - std::unique_ptr<ScriptedInterpolator>>> - m_scriptedInterpolatorInstances; + // The four data-binding / scripted-interpolator containers this instance + // used to hold inline (80 B, and 40 vs 56 of it depending on the standard + // library) now live in one heap struct, allocated on first use. See + // linear_animation_instance_extras.hpp for what is in it and why. `mutable` + // because both the scripted-interpolator and keyframe-value caches populate + // from the const apply() path. Deliberately not copied by the copy ctor — + // a copied LAI starts with an empty cluster. + mutable std::unique_ptr<LAIBindingExtras> m_bindingExtras; - // Data binds that cloneProperties() appended to m_artboardInstance on - // our behalf for the cloned ScriptedInterpolators above. We must - // removeDataBind+delete each of these in ~LinearAnimationInstance BEFORE - // m_scriptedInterpolatorInstances tears down, because the bind targets - // point at CustomPropertys owned by the clones. Captured by snapshotting - // m_artboardInstance->dataBinds().size() before/after each cloneScripted- - // Object call (addDataBind only ever appends). `mutable` so it can - // populate from the const apply() path. Not copied by the copy ctor. - mutable std::vector<DataBind*> m_clonedArtboardDataBinds; - - // Per-keyframe holders receiving data-bound values for this instance, keyed - // by the shared KeyFrame*. Built lazily by keyFrameValueHolder() on first - // apply of a bound keyframe; owned here and deleted in the destructor - // (after the clones targeting them are removed, below). Lazy unique_ptr => - // the common case (no bound keyframes) is an 8 B null pointer, no inline - // map. `mutable` so keyFrameValueHolder() can populate it from const - // apply(). Not copied by the copy ctor — a copied LAI starts unbound. - mutable std::unique_ptr< - std::unordered_map<const KeyFrame*, BindableProperty*>> - m_keyFrameValueHolders; - - // Clones of the source keyframe data binds, retargeted to the holders above - // and appended to m_artboardInstance's data-bind container, keyed by the - // keyframe so keyFrameValueHolder can refresh a holder at read time. - // Removed - // + deleted in ~LinearAnimationInstance BEFORE the holders they target, - // same teardown discipline as m_clonedArtboardDataBinds. `mutable` for the - // const apply() path; not copied by the copy ctor. - mutable std::unordered_map<const KeyFrame*, DataBind*> m_keyFrameValueBinds; + // Allocates the cold cluster on first use and returns it. + LAIBindingExtras& ensureBindingExtras() const; // Lazily clones the source bind onto a holder and parks it on the artboard. BindableProperty* buildKeyFrameValueHolder(const KeyFrame* keyframe,
diff --git a/include/rive/animation/linear_animation_instance_extras.hpp b/include/rive/animation/linear_animation_instance_extras.hpp new file mode 100644 index 0000000..81e72de --- /dev/null +++ b/include/rive/animation/linear_animation_instance_extras.hpp
@@ -0,0 +1,64 @@ +#ifndef _RIVE_LINEAR_ANIMATION_INSTANCE_EXTRAS_HPP_ +#define _RIVE_LINEAR_ANIMATION_INSTANCE_EXTRAS_HPP_ + +// Implementation detail of LinearAnimationInstance. Only +// linear_animation_instance.cpp needs it, and linear_animation_instance.hpp +// forward declares LAIBindingExtras rather than including this, so the hash +// containers below stay out of the public surface. +// +// LinearAnimationInstance is embedded *by value* in AnimationStateInstance and, +// N times over, in a blend state's BlendStateAnimationInstance vector, so every +// inline byte is multiplied by the live state instances in a file. The members +// here are populated only when a file authors a scripted interpolator or a +// data-bound keyframe value. Before adding an inline member there, check +// whether it belongs in here instead. + +#include <memory> +#include <unordered_map> +#include <vector> + +namespace rive +{ +class BindableProperty; +class DataBind; +class InterpolatingKeyFrame; +class KeyFrame; +class ScriptedInterpolator; + +/// The cold cluster of LinearAnimationInstance. Allocated the first time this +/// instance vends a stateful scripted interpolator or a data-bound keyframe +/// value; never allocated otherwise. +/// +/// Teardown of these four is order sensitive — see the comment in +/// ~LinearAnimationInstance, which does it explicitly rather than leaning on +/// declaration order. +struct LAIBindingExtras +{ + /// Per-(this LAI, keyframe) stateful clones of the shared + /// ScriptedInterpolator templates. The unique_ptr destroys the clone (and + /// its Lua ref) with this cluster. + std::unordered_map<const InterpolatingKeyFrame*, + std::unique_ptr<ScriptedInterpolator>> + scriptedInterpolators; + + /// Data binds that cloneProperties() appended to the artboard on our + /// behalf for the clones above. ~LinearAnimationInstance must + /// removeDataBind + delete each of these BEFORE `scriptedInterpolators` + /// tears down, because the bind targets point at CustomPropertys owned by + /// the clones. + std::vector<DataBind*> clonedArtboardDataBinds; + + /// Per-keyframe holders receiving data-bound values for this instance, + /// keyed by the shared KeyFrame*. Built lazily by keyFrameValueHolder() on + /// first apply of a bound keyframe; owned here. + std::unordered_map<const KeyFrame*, BindableProperty*> keyFrameValueHolders; + + /// Clones of the source keyframe data binds, retargeted to the holders + /// above and appended to the artboard's data-bind container, keyed by the + /// keyframe so keyFrameValueHolder can refresh a holder at read time. + /// Removed + deleted BEFORE the holders they target, same discipline as + /// `clonedArtboardDataBinds`. + std::unordered_map<const KeyFrame*, DataBind*> keyFrameValueBinds; +}; +} // namespace rive +#endif
diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp index 08eba6c..6c3a6a5 100644 --- a/src/animation/linear_animation_instance.cpp +++ b/src/animation/linear_animation_instance.cpp
@@ -19,6 +19,7 @@ #include "rive/data_bind/converters/data_converter.hpp" #include "rive/profiler/profiler_macros.h" #include "rive/scripted/scripted_interpolator.hpp" +#include "rive/animation/linear_animation_instance_extras.hpp" #include <cmath> #include <cassert> @@ -56,16 +57,25 @@ LinearAnimationInstance::~LinearAnimationInstance() { + if (m_bindingExtras == nullptr) + { + return; + } + auto& extras = *m_bindingExtras; // Critical teardown order, mirroring the SMI pattern at // state_machine_instance.cpp:2011-2044: pull cloned data binds out of - // the artboard and delete them BEFORE m_scriptedInterpolatorInstances + // the artboard and delete them BEFORE extras.scriptedInterpolators // destroys the clones whose CustomPropertys are those binds' targets. // Without this, the next Artboard::updateDataBinds() (which runs every // frame from updatePass) reads through DataBind::target() into freed // memory. + // + // The four containers now share one struct, so declaration order alone + // would decide this. Do it explicitly instead — the ordering is load + // bearing and must not be silently broken by reordering a field. if (m_artboardInstance != nullptr) { - for (auto* bind : m_clonedArtboardDataBinds) + for (auto* bind : extras.clonedArtboardDataBinds) { m_artboardInstance->removeDataBind(bind); delete bind; @@ -75,26 +85,26 @@ // scripted-interpolator teardown above). ~Artboard deletes m_Objects — // which own the LAIs (nested animations, joysticks) — before // deleteDataBinds(), so the artboard's bind list is still valid here. - for (auto& pair : m_keyFrameValueBinds) + for (auto& pair : extras.keyFrameValueBinds) { m_artboardInstance->removeDataBind(pair.second); delete pair.second; } } - m_clonedArtboardDataBinds.clear(); - m_keyFrameValueBinds.clear(); - // m_scriptedInterpolatorInstances destructs here via unique_ptr; safe now - // that no DataBind still points at the clones' CustomPropertys. + extras.clonedArtboardDataBinds.clear(); + extras.keyFrameValueBinds.clear(); // Keyframe value holders are owned here; safe to delete now that the clones - // targeting them were removed above. The unique_ptr frees the map itself. - if (m_keyFrameValueHolders != nullptr) + // targeting them were removed above. + for (auto& pair : extras.keyFrameValueHolders) { - for (auto& pair : *m_keyFrameValueHolders) - { - delete pair.second; - } + delete pair.second; } + extras.keyFrameValueHolders.clear(); + + // extras.scriptedInterpolators destructs with the cluster below; safe now + // that no DataBind still points at the clones' CustomPropertys. + m_bindingExtras.reset(); } // The BindableProperty value property key matching a keyframe's value type, or @@ -134,23 +144,33 @@ } } +LAIBindingExtras& LinearAnimationInstance::ensureBindingExtras() const +{ + if (m_bindingExtras == nullptr) + { + m_bindingExtras = std::make_unique<LAIBindingExtras>(); + } + return *m_bindingExtras; +} + BindableProperty* LinearAnimationInstance::keyFrameValueHolder( const KeyFrame* keyframe) const { // Already resolved for this LAI (holder cached). - if (m_keyFrameValueHolders != nullptr) + if (m_bindingExtras != nullptr) { - auto it = m_keyFrameValueHolders->find(keyframe); - if (it != m_keyFrameValueHolders->end()) + auto& holders = m_bindingExtras->keyFrameValueHolders; + auto it = holders.find(keyframe); + if (it != holders.end()) { // Refresh the holder from its source now (if the source changed // this frame) so the value is current at read time, regardless of // where the batched artboard updateDataBinds() falls relative to // the animation apply that's calling us. No-op when the bind isn't // dirty. - auto bindIt = m_keyFrameValueBinds.find(keyframe); - if (bindIt != m_keyFrameValueBinds.end() && - m_artboardInstance != nullptr) + auto& binds = m_bindingExtras->keyFrameValueBinds; + auto bindIt = binds.find(keyframe); + if (bindIt != binds.end() && m_artboardInstance != nullptr) { m_artboardInstance->flushDataBind(bindIt->second); } @@ -187,12 +207,7 @@ return nullptr; } BindableProperty* holder = makeKeyFrameValueHolder(keyframe->coreType()); - if (m_keyFrameValueHolders == nullptr) - { - m_keyFrameValueHolders = std::make_unique< - std::unordered_map<const KeyFrame*, BindableProperty*>>(); - } - (*m_keyFrameValueHolders)[keyframe] = holder; + ensureBindingExtras().keyFrameValueHolders[keyframe] = holder; // Clone the source bind, retarget it at the per-instance holder, and park // it on the artboard's data-bind container so it's advanced each frame. @@ -207,7 +222,7 @@ clone->converter(sourceBind->converter()->clone()->as<DataConverter>()); } m_artboardInstance->addDataBind(clone); - m_keyFrameValueBinds[keyframe] = clone; + m_bindingExtras->keyFrameValueBinds[keyframe] = clone; return holder; } @@ -225,13 +240,7 @@ { return nullptr; } - if (m_scriptedInterpolatorInstances == nullptr) - { - m_scriptedInterpolatorInstances = std::make_unique< - std::unordered_map<const InterpolatingKeyFrame*, - std::unique_ptr<ScriptedInterpolator>>>(); - } - auto& map = *m_scriptedInterpolatorInstances; + auto& map = ensureBindingExtras().scriptedInterpolators; auto it = map.find(keyframe); if (it != map.end()) { @@ -263,7 +272,7 @@ { if (auto* bind = input->dataBind()) { - m_clonedArtboardDataBinds.push_back(bind); + m_bindingExtras->clonedArtboardDataBinds.push_back(bind); } } }
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 26648c5..d5c63c6 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp
@@ -139,40 +139,36 @@ delete m_stateFrom; } - void init(StateMachineInstance* stateMachineInstance, - const StateMachineLayer* layer, - ArtboardInstance* instance) + /// The artboard every layer of this instance applies to. This is + /// identical for all layers of a given StateMachineInstance — as was the + /// owning instance pointer — so holding either per layer stored the same + /// value layerCount times over. Both are therefore derived from the `smi` + /// threaded through the methods below rather than stored per layer. + /// + /// The layer *definition* is deliberately NOT derived this way. It is + /// genuinely per-index data, and while `m_machine->layer(this - m_layers)` + /// would recover it, that lookup is only stable in runtime builds. Under + /// WITH_RIVE_EDITOR, StateMachine::layer() reads `m_editorLayers`, which + /// EditorFile::finalizeBatch clears and rebuilds from arena order after + /// every coop batch, while clearStalePlaybackScenes only rebuilds the + /// StateMachineInstance when the StateMachine *pointer* changes. So adding + /// or reparenting a layer can leave slot i resolving to a different + /// definition — or, if a layer was deleted, to nullptr — while m_layers[i] + /// still holds the old layer's runtime state. m_layer is captured once at + /// init and pinned for the instance's lifetime instead. + static ArtboardInstance* artboardOf(const StateMachineInstance* smi) { - - if (File::deterministicMode) - { - srand((unsigned int)1); - } - else - { - auto now = std::chrono::high_resolution_clock::now(); - auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>( - now.time_since_epoch()) - .count(); - srand((unsigned int)nanos); - } - m_stateMachineInstance = stateMachineInstance; - m_artboardInstance = instance; - assert(m_layer == nullptr); - // A layer without an any state is degenerate but not fatal: every - // other use of m_anyStateInstance is either a delete guard or a - // tryChangeState call, both of which handle null. Keeping this - // tolerant is what lets StateMachineLayer stop requiring the state - // to be present, so exports can eventually omit unused ones. - auto anyState = layer->anyState(); - m_anyStateInstance = anyState == nullptr - ? nullptr - : anyState->makeInstance(instance).release(); - m_layer = layer; - changeState(m_layer->entryState()); + return smi->m_artboardInstance; } - void resetState() + void init(StateMachineInstance* smi, const StateMachineLayer* layer) + { + assert(m_layer == nullptr); + m_layer = layer; + changeState(smi, m_layer->entryState()); + } + + void resetState(StateMachineInstance* smi) { if (m_stateFrom != m_anyStateInstance && m_stateFrom != m_currentState) { @@ -184,10 +180,10 @@ delete m_currentState; } m_currentState = nullptr; - changeState(m_layer->entryState()); + changeState(smi, m_layer->entryState()); } - void updateMix(float seconds) + void updateMix(StateMachineInstance* smi, float seconds) { if (m_transition != nullptr && m_stateFrom != nullptr && resolvedDuration() != 0) @@ -206,9 +202,11 @@ { m_transitionCompleted = true; clearAnimationReset(); - fireEvents(StateMachineFireOccurance::atEnd, + fireEvents(smi, + StateMachineFireOccurance::atEnd, m_transition->events()); - performListenerActions(StateMachineFireOccurance::atEnd, + performListenerActions(smi, + StateMachineFireOccurance::atEnd, m_transition->listenerActions()); } } @@ -218,45 +216,42 @@ } } - bool advance(float seconds, bool newFrame) + bool advance(StateMachineInstance* smi, float seconds, bool newFrame) { if (newFrame) { m_stateMachineChangedOnAdvance = false; } - m_currentState->advance(seconds, m_stateMachineInstance); - updateMix(seconds); + m_currentState->advance(seconds, smi); + updateMix(smi, seconds); if (m_stateFrom != nullptr && m_mix < 1.0f && !m_holdAnimationFrom) { // This didn't advance during our updateState, but it should now // that we realize we need to mix it in. - m_stateFrom->advance(seconds, m_stateMachineInstance); + m_stateFrom->advance(seconds, smi); } - apply(); + apply(smi); bool changedState = false; - for (int i = 0; updateState(); i++) + for (int i = 0; updateState(smi); i++) { changedState = true; - apply(); + apply(smi); if (i == maxIterations) { auto stateMachineName = - m_stateMachineInstance->stateMachine() == nullptr + smi->stateMachine() == nullptr ? "[SM Not found]" - : m_stateMachineInstance->stateMachine() - ->name() - .c_str(); + : smi->stateMachine()->name().c_str(); auto layerName = m_layer == nullptr ? "[LY Not found]" : m_layer->name().c_str(); - auto artboardName = - m_stateMachineInstance->artboard() == nullptr - ? "[AB Not found]" - : m_stateMachineInstance->artboard()->name().c_str(); + auto artboardName = smi->artboard() == nullptr + ? "[AB Not found]" + : smi->artboard()->name().c_str(); fprintf(stderr, "%s StateMachine exceeded max iterations in layer %s " "on artboard %s\n", @@ -317,7 +312,32 @@ resolvedDuration() != 0 && m_mix < 1.0f; } - bool updateState() + /// The any state's instance is only ever fed to tryChangeState, so a layer + /// whose any state has no transitions never needs one. Most don't, so this + /// is built on demand instead of at init: it saves a heap allocation per + /// layer in the common case. Lazy rather than a one-shot check at init + /// because LayerState::transitionCount() also reports the editor's + /// live-edit list, which can grow after this instance was built. + void ensureAnyStateInstance(StateMachineInstance* smi) + { + if (m_anyStateInstance != nullptr) + { + return; + } + // A layer without an any state is degenerate but not fatal: every + // other use of m_anyStateInstance is either a delete guard or a + // tryChangeState call, both of which handle null. Keeping this + // tolerant is what lets StateMachineLayer stop requiring the state + // to be present, so exports can eventually omit unused ones. + auto anyState = m_layer == nullptr ? nullptr : m_layer->anyState(); + if (anyState == nullptr || anyState->transitionCount() == 0) + { + return; + } + m_anyStateInstance = anyState->makeInstance(artboardOf(smi)).release(); + } + + bool updateState(StateMachineInstance* smi) { // Don't allow changing state while a transition is taking place // (we're mixing one state onto another) if enableEarlyExit is not true. @@ -328,27 +348,30 @@ m_waitingForExit = false; - if (tryChangeState(m_anyStateInstance)) + ensureAnyStateInstance(smi); + if (tryChangeState(smi, m_anyStateInstance)) { return true; } - return tryChangeState(m_currentState); + return tryChangeState(smi, m_currentState); } - void fireEvents(StateMachineFireOccurance occurs, + void fireEvents(StateMachineInstance* smi, + StateMachineFireOccurance occurs, const std::vector<StateMachineFireAction*>& fireEvents) { for (auto event : fireEvents) { if (event->occurs() == occurs) { - event->perform(m_stateMachineInstance); + event->perform(smi); } } } void performListenerActions( + StateMachineInstance* smi, StateMachineFireOccurance occurs, const std::vector<std::unique_ptr<ListenerAction>>& listenerActions) { @@ -356,8 +379,7 @@ { if (action->matchesScheduledOccurrence(occurs)) { - action->perform(m_stateMachineInstance, - ListenerInvocation::none()); + action->perform(smi, ListenerInvocation::none()); } } } @@ -371,7 +393,7 @@ double randomValue() { return RandomProvider::generateRandomFloat(); } - void changeState(const LayerState* stateTo) + void changeState(StateMachineInstance* smi, const LayerState* stateTo) { if ((m_currentState == nullptr ? nullptr : m_currentState->state()) == stateTo) @@ -382,29 +404,33 @@ // Fire end events for the state we're changing from. if (m_currentState != nullptr) { - fireEvents(StateMachineFireOccurance::atEnd, + fireEvents(smi, + StateMachineFireOccurance::atEnd, m_currentState->state()->events()); - performListenerActions(StateMachineFireOccurance::atEnd, + performListenerActions(smi, + StateMachineFireOccurance::atEnd, m_currentState->state()->listenerActions()); } - m_currentState = - stateTo == nullptr - ? nullptr - : stateTo->makeInstance(m_artboardInstance).release(); + m_currentState = stateTo == nullptr + ? nullptr + : stateTo->makeInstance(artboardOf(smi)).release(); // Fire start events for the state we're changing to. if (m_currentState != nullptr) { - fireEvents(StateMachineFireOccurance::atStart, + fireEvents(smi, + StateMachineFireOccurance::atStart, m_currentState->state()->events()); - performListenerActions(StateMachineFireOccurance::atStart, + performListenerActions(smi, + StateMachineFireOccurance::atStart, m_currentState->state()->listenerActions()); } return; } - StateTransition* findRandomTransition(StateInstance* stateFromInstance) + StateTransition* findRandomTransition(StateMachineInstance* smi, + StateInstance* stateFromInstance) { uint32_t totalWeight = 0; auto stateFrom = stateFromInstance->state(); @@ -415,9 +441,8 @@ if (canChangeState(transition->stateTo())) { - auto allowed = transition->allowed(stateFromInstance, - m_stateMachineInstance, - this); + auto allowed = + transition->allowed(stateFromInstance, smi, this); if (allowed == AllowTransition::yes) { transition->evaluatedRandomWeight( @@ -451,8 +476,7 @@ (double)transition->evaluatedRandomWeight(); if (currentWeight + transitionWeight > randomWeight) { - transition->useLayerInConditions(m_stateMachineInstance, - this); + transition->useLayerInConditions(smi, this); return transition; } currentWeight += transitionWeight; @@ -462,14 +486,15 @@ return nullptr; } - StateTransition* findAllowedTransition(StateInstance* stateFromInstance) + StateTransition* findAllowedTransition(StateMachineInstance* smi, + StateInstance* stateFromInstance) { auto stateFrom = stateFromInstance->state(); // If it should randomize if ((static_cast<LayerStateFlags>(stateFrom->flags()) & LayerStateFlags::Random) == LayerStateFlags::Random) { - return findRandomTransition(stateFromInstance); + return findRandomTransition(smi, stateFromInstance); } // Else search the first valid transition for (size_t i = 0, length = stateFrom->transitionCount(); i < length; @@ -479,15 +504,13 @@ if (canChangeState(transition->stateTo())) { - auto allowed = transition->allowed(stateFromInstance, - m_stateMachineInstance, - this); + auto allowed = + transition->allowed(stateFromInstance, smi, this); if (allowed == AllowTransition::yes) { transition->evaluatedRandomWeight( transition->randomWeight()); - transition->useLayerInConditions(m_stateMachineInstance, - this); + transition->useLayerInConditions(smi, this); return transition; } else @@ -503,12 +526,11 @@ return nullptr; } - void buildAnimationResetForTransition() + void buildAnimationResetForTransition(StateMachineInstance* smi) { - m_animationReset = - AnimationResetFactory::fromStates(m_stateFrom, - m_currentState, - m_artboardInstance); + m_animationReset = AnimationResetFactory::fromStates(m_stateFrom, + m_currentState, + artboardOf(smi)); } void clearAnimationReset() @@ -520,44 +542,48 @@ } } - bool tryChangeState(StateInstance* stateFromInstance) + bool tryChangeState(StateMachineInstance* smi, + StateInstance* stateFromInstance) { if (stateFromInstance == nullptr) { return false; } auto outState = m_currentState; - auto transition = findAllowedTransition(stateFromInstance); + auto transition = findAllowedTransition(smi, stateFromInstance); if (transition != nullptr) { clearAnimationReset(); - changeState(transition->stateTo()); + changeState(smi, transition->stateTo()); m_stateMachineChangedOnAdvance = true; #ifdef RIVE_MICROPROFILE RiveProfile::instance().recordTransition( - m_stateMachineInstance->artboard()->name(), - m_stateMachineInstance->name(), + smi->artboard()->name(), + smi->name(), m_layer->name(), getStateName(outState), getStateName(m_currentState), - m_stateMachineInstance->artboard()); + smi->artboard()); #endif // state actually has changed m_transition = transition; - m_transitionDurationProperty = - m_stateMachineInstance->findTransitionPropertyInstance( - transition, - StateTransitionBase::durationPropertyKey); - fireEvents(StateMachineFireOccurance::atStart, + m_transitionDurationProperty = smi->findTransitionPropertyInstance( + transition, + StateTransitionBase::durationPropertyKey); + fireEvents(smi, + StateMachineFireOccurance::atStart, transition->events()); - performListenerActions(StateMachineFireOccurance::atStart, + performListenerActions(smi, + StateMachineFireOccurance::atStart, transition->listenerActions()); if (resolvedDuration() == 0) { m_transitionCompleted = true; - fireEvents(StateMachineFireOccurance::atEnd, + fireEvents(smi, + StateMachineFireOccurance::atEnd, transition->events()); - performListenerActions(StateMachineFireOccurance::atEnd, + performListenerActions(smi, + StateMachineFireOccurance::atEnd, transition->listenerActions()); } else @@ -574,7 +600,7 @@ if (!m_transitionCompleted) { - buildAnimationResetForTransition(); + buildAnimationResetForTransition(smi); } // If we had an exit time and wanted to pause on exit, make @@ -613,25 +639,26 @@ advanceTime = instance->spilledTime(); } } - m_currentState->advance(advanceTime, m_stateMachineInstance); + m_currentState->advance(advanceTime, smi); } m_mix = 0.0f; - updateMix(0.0f); + updateMix(smi, 0.0f); m_waitingForExit = false; return true; } return false; } - void apply(/*Artboard* artboard*/) + void apply(StateMachineInstance* smi) { + auto artboardInstance = artboardOf(smi); if (m_animationReset != nullptr) { - m_animationReset->apply(m_artboardInstance); + m_animationReset->apply(artboardInstance); } if (m_holdAnimation != nullptr) { - m_holdAnimation->apply(m_artboardInstance, m_holdTime, m_mixFrom); + m_holdAnimation->apply(artboardInstance, m_holdTime, m_mixFrom); m_holdAnimation = nullptr; } @@ -646,13 +673,13 @@ auto fromMix = interpolator != nullptr ? interpolator->transform(m_mixFrom) : m_mixFrom; - m_stateFrom->apply(m_artboardInstance, fromMix); + m_stateFrom->apply(artboardInstance, fromMix); } if (m_currentState != nullptr) { auto mix = interpolator != nullptr ? interpolator->transform(m_mix) : m_mix; - m_currentState->apply(m_artboardInstance, mix); + m_currentState->apply(artboardInstance, mix); } } @@ -679,10 +706,16 @@ private: static const int maxIterations = 100; - StateMachineInstance* m_stateMachineInstance = nullptr; - const StateMachineLayer* m_layer = nullptr; - ArtboardInstance* m_artboardInstance = nullptr; + // One of these exists per layer of every StateMachineInstance, which in an + // ArtboardComponentList means per layer per row. Keep the pointers, then + // the floats, then the bools: interleaving them costs 8 B of padding for + // nothing. The owning instance and its artboard used to be stored here + // too; both are the same for every layer of an instance, so they are now + // derived from the `smi` argument threaded through the methods above. The + // layer definition stays stored — see artboardOf() for why deriving it + // from the array index is not safe in editor builds. + const StateMachineLayer* m_layer = nullptr; StateInstance* m_anyStateInstance = nullptr; StateInstance* m_currentState = nullptr; StateInstance* m_stateFrom = nullptr; @@ -690,18 +723,17 @@ const StateTransition* m_transition = nullptr; BindablePropertyNumber* m_transitionDurationProperty = nullptr; std::unique_ptr<AnimationReset> m_animationReset = nullptr; - bool m_transitionCompleted = false; - - bool m_holdAnimationFrom = false; + /// Used to ensure a specific animation is applied on the next apply. + const LinearAnimation* m_holdAnimation = nullptr; float m_mix = 1.0f; float m_mixFrom = 1.0f; - bool m_stateMachineChangedOnAdvance = false; - - bool m_waitingForExit = false; - /// Used to ensure a specific animation is applied on the next apply. - const LinearAnimation* m_holdAnimation = nullptr; float m_holdTime = 0.0f; + + bool m_transitionCompleted = false; + bool m_holdAnimationFrom = false; + bool m_stateMachineChangedOnAdvance = false; + bool m_waitingForExit = false; }; /// Representation of a Component from the Artboard Instance and all the @@ -1772,11 +1804,27 @@ #endif } + // Seeded once per state machine instance. This used to run inside the + // per-layer init(), reseeding the global RNG (and, outside deterministic + // mode, reading the clock) once for every layer of every instance. + if (File::deterministicMode) + { + srand((unsigned int)1); + } + else + { + auto now = std::chrono::high_resolution_clock::now(); + auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>( + now.time_since_epoch()) + .count(); + srand((unsigned int)nanos); + } + m_layerCount = static_cast<uint32_t>(machine->layerCount()); m_layers = new StateMachineLayerInstance[m_layerCount]; for (size_t i = 0; i < m_layerCount; i++) { - m_layers[i].init(this, machine->layer(i), m_artboardInstance); + m_layers[i].init(this, machine->layer(i)); } // Initialize dataBinds. All databinds are cloned for the state machine @@ -2391,7 +2439,7 @@ bool hasChangedState = false; for (size_t i = 0; i < m_layerCount; i++) { - if (m_layers[i].updateState()) + if (m_layers[i].updateState(this)) { hasChangedState = true; } @@ -2710,7 +2758,7 @@ updateDataBinds(false); for (size_t i = 0; i < m_layerCount; i++) { - if (m_layers[i].advance(seconds, newFrame)) + if (m_layers[i].advance(this, seconds, newFrame)) { m_needsAdvance = true; } @@ -2840,7 +2888,7 @@ { for (size_t i = 0; i < m_layerCount; i++) { - m_layers[i].resetState(); + m_layers[i].resetState(this); } }