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);
     }
 }