perf(runtime): Idle state machines stop redoing work that can't change anything (#14508) 30c30acec9 * perf(runtime): skip transition searches on settled state machine layers Every layer of every state machine searched all of its transitions at least twice a frame, even when nothing its conditions read had changed. In an idle 400-row component list with view model conditions, that search was ~40% of the frame. A layer whose search finds nothing now settles: later searches return at once until its state changes or something its conditions read does. Only states whose transitions use view model conditions alone can settle (classified once per state in StateMachine::onAddedClean). State machine inputs, random states, and component, artboard, focus and scripted conditions keep the full search. Settled layers wake on any dirtied state machine data bind, on data context changes (new DataBindContainer::dataContextChanged), and on main view model swaps; none settles while a dirtied bind is still unapplied. Editor builds never settle, since transitions change there after import. Memory: StateMachineLayerInstance stays 80 B (its flags are now bit-fields) and StateMachineInstance 288 B (two bools in existing padding). LayerState grows 104 -> 112 B per state definition. Under TESTING every skipped search is re-run and aborts if it would have transitioned: the full suite checks 1.7M skips with no mismatch. ArtboardFrame benches: a 400-tile list scene -33% idle (-22% with the RiveRenderer), bidirectional_binding_idle -85%, the rest neutral. * perf(runtime): freeze settled layers in states that key nothing A settled layer still advanced and applied its current state every frame. For entry, exit and any states, and for animation states whose animation keys nothing (including the empty animation that stands in for a missing one), that work only moves a clock and applies no values. In an idle 400-row component list it was ~30% of the frame. Such a layer is now frozen: once settled, fully mixed and no longer keeping itself going, its advance returns what it would have without advancing, mixing, applying or searching. The one reader of that clock is exit time, so a state with any enabled exit-time transition never freezes (LayerState::transitionsIgnoreTime, classified in onAddedClean). Whether a state keys anything is checked on the live instance instead: an artboard's last layer only resolves its animations once the next layer is read, which can be after the artboard initializes. No size change: the new LayerState flag sits in existing padding. Idle-frame benches vs settled layers alone: a 400-tile list scene -31% steady-state advance, -22% advance + draw (-47% vs master); bidirectional_binding_idle -24%; the rest neutral. * perf(runtime): add state machine bind clones once their target is set StateMachineInstance added each cloned data bind before pointing it at its per-instance bindable. addDataBind then saw no target, decided a toSource bind couldn't push, and put it on the polling list, so every listener bind in every state machine was re-read on each updateDataBinds pass. The state machine never applies target to source in those passes (it always passes false; listener actions push their value explicitly), so the polling did nothing but keep updateDataBinds from returning early. Adding the clone after its target is set puts such binds on the push path: they are processed only when the listener's bindable actually changes. * perf(runtime): skip the per-frame work of quiet list rows Every row of an ArtboardComponentList advanced its state machine, searched its transitions, updated its data binds, advanced its artboard and reset it several times a frame, even when none of that did anything. With 400 settled, frozen tile rows that bookkeeping was ~95% of the frame. A row whose per-frame work would do nothing is now marked quiet and skipped by every pass (advance, settle, updateDataBinds, reset, and the update pass) until something wakes it. Quiet means: its state machine's layers are all frozen with nothing pending (inputs, binds, reports, focus and semantic queues, draw order), and its artboard has no dirt, bind work, layout or host transform change, and only advancing components that report being idle (AdvancingComponent::quietState). Rows with scripts, nested artboards, joysticks, resettables, text inputs, scroll constraints or converters that advance never go quiet, and neither do rows of lists with duplicate items. Unrealized rows count as quiet: they have no work until realized. Rows are woken through Artboard::wakeIfQuietRow, from the artboard's dirt, data binds, data context, layout and host transform, and from its state machine (markNeedsAdvance, focus and semantic queues, reports, unsettling, reset) and every listener's performChanges, which keyboard and text listeners otherwise use without marking anything. Rows are reset before their instances change (updateList, setRowsForItem, addArtboardAt, clear). The list keeps one bit per row, so passes step over quiet rows 64 at a time. Item view models of number-bound lists are still advanced for every row: skipping that needs a changed flag on view model instances. Memory: Artboard gains a row index in existing padding (size unchanged); each list gains two bit vectors. Under TESTING every skipped row pass still runs after the pass and aborts if the row stopped being quiet without waking or did any work; the suite checks 22,847 of them. Idle-frame benches vs frozen layers: a 400-tile list scene -92% steady-state advance (68 -> 5.6 us), -60% advance + draw; component_list_virtualized_long -16%, its scroll -4%; the rest neutral. * fix(runtime): don't wake a quiet row from a state machine's destructor A state machine's destructor unbinds its data context, which unsettles its layers and so woke its artboard's quiet list row, reading the artboard. When an ArtboardComponentList cleared its pools it destroyed the pooled artboards before the pooled state machines, so that read hit freed memory (caught by ASan in "Artboard lists with child artboard origin offset"). A host can also destroy an artboard before its state machine. The state machine now stops waking its row once its destructor starts, and the list destroys pooled state machines before pooled artboards, as it already does for its realized rows. m_bindsPending, m_anySettled and the new m_destroying are bit-fields sharing the byte after m_needsAdvance, so StateMachineInstance stays 288 B. Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head index 2ef8e90..1b52e3e 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -4c2d79894f8dfd8dd8cca0aa6d4a44504cdc51b9 +30c30acec9ddcb98f7a85cd53cf7065fa4bcc189
diff --git a/include/rive/advancing_component.hpp b/include/rive/advancing_component.hpp index dc04653..7c1fe7d 100644 --- a/include/rive/advancing_component.hpp +++ b/include/rive/advancing_component.hpp
@@ -6,6 +6,7 @@ namespace rive { class Component; +class Core; class AdvancingComponent { public: @@ -13,6 +14,19 @@ float elapsedSeconds, AdvanceFlags flags = AdvanceFlags::Animate | AdvanceFlags::NewFrame) = 0; + + // Whether advanceComponent would currently do nothing: `quiet` when it + // would return false having changed nothing, `busy` when it would do work. + // `never` is for components that can't say, which keeps the artboard + // hosting them from ever being skipped (see ArtboardComponentList's quiet + // rows). + enum class QuietState + { + never, + busy, + quiet, + }; + virtual QuietState quietState() { return QuietState::never; } static AdvancingComponent* from(Core* component); }; } // namespace rive
diff --git a/include/rive/animation/layer_state.hpp b/include/rive/animation/layer_state.hpp index 7d6daab..a1ace7d 100644 --- a/include/rive/animation/layer_state.hpp +++ b/include/rive/animation/layer_state.hpp
@@ -58,6 +58,22 @@ virtual std::unique_ptr<StateInstance> makeInstance( ArtboardInstance* instance) const; + /// Whether every condition on this state's transitions only reads values + /// whose changes reach the state machine instance (view model values it + /// binds, and its data context). A layer whose search found nothing from + /// such a state can skip searching again until one of them changes. Set + /// once by StateMachine::onAddedClean; never set in editor builds, whose + /// transitions change after import. + bool transitionsSettleSafe() const { return m_transitionsSettleSafe; } + void transitionsSettleSafe(bool value) { m_transitionsSettleSafe = value; } + + /// Whether none of this state's transitions waits on an exit time, the only + /// thing that reads how long the state has been active. A settled layer in + /// such a state that keys nothing can skip advancing it. Set with + /// transitionsSettleSafe. + bool transitionsIgnoreTime() const { return m_transitionsIgnoreTime; } + void transitionsIgnoreTime(bool value) { m_transitionsIgnoreTime = value; } + #ifdef WITH_RIVE_EDITOR // Editor-only parallel non-owning transition list. `m_Transitions` // is owned by `LayerState::~LayerState`. See @@ -71,6 +87,8 @@ #ifdef WITH_RIVE_EDITOR std::vector<StateTransition*> m_editorTransitions; #endif + bool m_transitionsSettleSafe = false; + bool m_transitionsIgnoreTime = false; }; } // namespace rive
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index 387c1e2..cf905db 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp
@@ -5,6 +5,7 @@ #include <stddef.h> #include <vector> #include <unordered_map> +#include "rive/advancing_component.hpp" #include "rive/animation/gamepad_listener_group.hpp" #include "rive/animation/keyboard_listener_group.hpp" #include "rive/animation/semantic_listener_group.hpp" @@ -317,6 +318,16 @@ // The artboard bound through this machine is not a dependent of the // context itself, so context hooks reach it from here. void mainViewModelInstanceChanged() override; + void updateDataBinds(bool applyTargetToSource = true) override; + void addDirtyDataBind(DataBind* dataBind) override; + void dataContextChanged() override; + /// Whether this machine's per-frame work as a list row (advance, + /// tryChangeState and updateDataBinds) would currently do nothing. See + /// Artboard::rowQuietState. + AdvancingComponent::QuietState rowQuietState(); + // Something may have changed this machine: wake its artboard if a host + // had it quiet. + void wakeRow(); void dropInstanceValueBindsTargeting(Core* target) override; void internalDataContext(rcp<DataContext> dataContext); ScriptedObject* scriptedObject(const ScriptedObject*) const; @@ -417,6 +428,12 @@ return nullptr; } const LayerState* layerState(size_t index); + // Transition searches skipped because the layer had settled, across every + // instance (see StateMachineLayerInstance::updateState). + static uint64_t sm_settledLayerSkips; + // Layer advances skipped because the layer was frozen: settled in a state + // that keys nothing and waits on no exit time. + static uint64_t sm_frozenLayerAdvances; #endif void enablePointerEvents(int pointerId = 0); void disablePointerEvents(int pointerId = 0); @@ -425,13 +442,23 @@ private: const StateMachine* m_machine; std::vector<SMIInput*> m_inputInstances; // we own each pointer - StateMachineLayerInstance* m_layers; + StateMachineLayerInstance* m_layers = nullptr; // Set once from machine->layerCount() and never mutated, so a uint32_t is // ample. Kept adjacent to the other scalars so they share one 8 B slot // instead of opening three. uint32_t m_layerCount = 0; uint8_t m_drawOrderChangeCounter = 0; bool m_needsAdvance = false; + // One bit each, sharing the byte after m_needsAdvance; bit-fields can't + // have default member initializers before C++20, so the constructor sets + // them. Settled layers, see StateMachineLayerInstance::updateState: a bind + // was dirtied and not yet applied, so bindables may still hold old values. + bool m_bindsPending : 1; + // Some layer is settled, so unsettleLayers has work to do. + bool m_anySettled : 1; + // Set as the destructor starts. The artboard may already be gone (a host + // can destroy it first), so nothing may wake its row from then on. + bool m_destroying : 1; std::vector<std::unique_ptr<HitComponent>> m_hitComponents; std::vector<std::unique_ptr<ListenerGroup>> m_listenerGroups; // Owns the in-flight scroll gesture. Latched for the whole gesture: @@ -443,6 +470,9 @@ void unbind(); void removeEventListeners(); void initScriptedObjects(); + // Something a settled layer's conditions read may have changed: search + // every layer again on its next update. + void unsettleLayers(); // Cold clusters. See state_machine_instance_clusters.hpp. Sidecar<SMIReporting> m_reporting;
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp index b692c61..9db88ed 100644 --- a/include/rive/artboard.hpp +++ b/include/rive/artboard.hpp
@@ -132,6 +132,7 @@ // A resync asked for mid update waits for the pass to drain. bool m_instanceValueBindsPending = false; void syncInstanceValueBinds(); + void wakeQuietRow(); public: void mainViewModelInstanceChanged() override; @@ -148,6 +149,8 @@ Drawable* m_FirstDrawable = nullptr; bool m_IsInstance = false; bool m_FrameOrigin = true; + // In the padding after the flags above; see quietHostRow. + uint32_t m_quietHostRow = kNoQuietRow; std::unordered_set<LayoutComponent*> m_dirtyLayout; bool m_isCleaningDirtyLayouts = false; std::unique_ptr<KeyFrameInterpolator> m_ownedInheritedInterpolator; @@ -274,6 +277,8 @@ // Layout passes (calculateLayout calls) across every artboard, so a test // can tell a settled artboard from one that re-solves every frame. static uint64_t layoutPassCount() { return sm_layoutPassCount; } + // Dirt reported to any artboard (onComponentDirty and onDirty calls). + static uint64_t sm_dirtNotifications; static void incFrameId() { sm_frameId++; } #elif WITH_RIVE_TOOLS static void incFrameId() { sm_frameId++; } @@ -289,6 +294,28 @@ bool hasKeyFrameSourceBinds() const; void host(ArtboardHost* artboardHost); ArtboardHost* host() const; + + /// While a host skips this row artboard's per-frame work (see + /// ArtboardComponentList's quiet rows), the row it is quiet at, else + /// kNoQuietRow. Whatever may change the row wakes it: its dirt, its data + /// binds and data context, its layout and host transform, and its state + /// machine (StateMachineInstance forwards its own wake-ups here). + static constexpr uint32_t kNoQuietRow = 0xFFFFFFFF; + uint32_t quietHostRow() const { return m_quietHostRow; } + void quietHostRow(uint32_t row) { m_quietHostRow = row; } + void wakeIfQuietRow() + { + if (m_quietHostRow != kNoQuietRow) + { + wakeQuietRow(); + } + } + /// Whether this artboard's own per-frame work as a row (advanceInternal, + /// updateDataBinds, reset and updatePass) would currently do nothing. + /// `never` for artboards whose content can't say (hosted artboards, + /// scripts, joysticks, resettables, advancing converters, advancing + /// components that can't report). + AdvancingComponent::QuietState rowQuietState(); void addedToHost() { setLayoutFlag(LayoutComponentFlags::JustAddedToHost, true); @@ -565,6 +592,7 @@ void markLayoutDirty(LayoutComponent* layoutComponent); void markHostTransformDirty(); + void dataContextChanged() override; void cleanLayout(LayoutComponent* layoutComponent); LayoutData* takeLayoutData();
diff --git a/include/rive/artboard_component_list.hpp b/include/rive/artboard_component_list.hpp index 48269b2..efa5d3a 100644 --- a/include/rive/artboard_component_list.hpp +++ b/include/rive/artboard_component_list.hpp
@@ -77,6 +77,7 @@ void draw(Renderer* renderer) override; bool willDraw() override; Core* hitTest(HitInfo*, const Mat2D&) override; + void hostedRowWoke(Artboard* artboard, uint32_t row) override; void update(ComponentDirt value) override; void updateConstraints() override; void internalDataContext(rcp<DataContext> dataContext) override; @@ -127,7 +128,7 @@ m_realizedEndIndex = end; invalidateOrderedListIndicesCache(); } - void shouldResetInstances(bool value) { m_shouldResetInstances = value; } + void shouldResetInstances(bool value); void setVirtualizablePosition(int index, Vec2D position) override; void createArtboardAt(int index, bool forceLayoutSync = true); void addArtboardAt(std::unique_ptr<ArtboardInstance> artboard, @@ -260,6 +261,53 @@ bool m_shouldResetInstances = false; // Whether some item shows on more than one row. bool m_listHasDuplicateItems = false; + + // Quiet rows: rows whose per-frame work (their state machine's advance, + // tryChangeState and updateDataBinds, their artboard's advance, bind + // updates, reset and update pass) would do nothing are skipped until + // something wakes them (Artboard::quietHostRow). A bit per row, so a pass + // steps over 64 quiet rows at a time. + std::vector<uint64_t> m_quietRows; + // Rows whose content can't report being quiet, so they aren't checked + // again every frame. + std::vector<uint64_t> m_neverQuietRows; + // The first row at or after `row` that isn't quiet, or the row count. + size_t nextAwakeRow(size_t row) const; + bool isRowQuiet(size_t row) const; + // Marks the row quiet if its work would do nothing; true if it did. + bool tryQuietRow(size_t row); + // Whether the row's work would do nothing right now. + AdvancingComponent::QuietState rowQuietState(size_t row); + // Wakes the row and forgets it can't be quiet: its instances are about to + // change. + void resetQuietRow(size_t row); + // Wakes every row, then sizes the bits for `rowCount` rows. + void resetQuietRows(size_t rowCount); + enum class RowPass + { + advance, + settle, + updateDataBinds, + reset, + update, + }; +#ifdef TESTING + // Each quiet row's skipped work is still done in tests, after the pass, + // and must do nothing; a row still quiet must also still be quiet by + // rowQuietState, or a wake-up was missed. + void verifyQuietRows(RowPass pass, + float elapsedSeconds, + AdvanceFlags flags, + bool advanceNested); + +public: + // Row passes skipped because the row was quiet, across every list. + static uint64_t sm_quietRowSkips; + // Lets a test run the same frames with and without quiet rows. + static bool sm_quietRowsEnabled; + +private: +#endif bool listsAreEqual(std::vector<rcp<ViewModelInstanceListItem>>* list, std::vector<rcp<ViewModelInstanceListItem>>* compared);
diff --git a/include/rive/artboard_host.hpp b/include/rive/artboard_host.hpp index 8cbf370..7b2fb52 100644 --- a/include/rive/artboard_host.hpp +++ b/include/rive/artboard_host.hpp
@@ -24,6 +24,9 @@ virtual void unbind() = 0; virtual void updateDataBinds() = 0; virtual void markHostingLayoutDirty(ArtboardInstance* artboardInstance) {} + // A hosted artboard the host had marked quiet at `row` (see + // Artboard::quietHostRow) saw something that may change it. + virtual void hostedRowWoke(Artboard* artboard, uint32_t row) {} // The artboard that contains this ArtboardHost virtual Artboard* parentArtboard() = 0; virtual bool hitTestHost(const Vec2D& position,
diff --git a/include/rive/data_bind/converters/data_converter.hpp b/include/rive/data_bind/converters/data_converter.hpp index 86e3db8..10516bb 100644 --- a/include/rive/data_bind/converters/data_converter.hpp +++ b/include/rive/data_bind/converters/data_converter.hpp
@@ -28,6 +28,9 @@ virtual void update(); void copy(const DataConverter& object); virtual bool advance(float elapsedTime); + // Whether advance can do work: false for converters whose advance is the + // base no-op, so a bind using one never needs advancing. + virtual bool mayAdvance() const { return false; } void addDirtyDataBind(DataBind*) override; virtual void reset() {};
diff --git a/include/rive/data_bind/converters/data_converter_group.hpp b/include/rive/data_bind/converters/data_converter_group.hpp index b6829a7..b714430 100644 --- a/include/rive/data_bind/converters/data_converter_group.hpp +++ b/include/rive/data_bind/converters/data_converter_group.hpp
@@ -37,6 +37,7 @@ void unbind() override; void update() override; bool advance(float elapsedSeconds) override; + bool mayAdvance() const override; void reset() override; private:
diff --git a/include/rive/data_bind/converters/data_converter_interpolator.hpp b/include/rive/data_bind/converters/data_converter_interpolator.hpp index 2ccb008..963c35e 100644 --- a/include/rive/data_bind/converters/data_converter_interpolator.hpp +++ b/include/rive/data_bind/converters/data_converter_interpolator.hpp
@@ -127,6 +127,7 @@ DataValue* convert(DataValue* value, DataBind* dataBind) override; DataValue* reverseConvert(DataValue* value, DataBind* dataBind) override; bool advance(float elapsedTime) override; + bool mayAdvance() const override { return true; } void reset() override; void copy(const DataConverterInterpolatorBase& object); void durationChanged() override;
diff --git a/include/rive/data_bind/data_bind_container.hpp b/include/rive/data_bind/data_bind_container.hpp index 34da9e6..f5264ac 100644 --- a/include/rive/data_bind/data_bind_container.hpp +++ b/include/rive/data_bind/data_bind_container.hpp
@@ -85,6 +85,19 @@ virtual void relinkDataContext() {}; // The bound context swapped its main instance without a rebind. virtual void mainViewModelInstanceChanged() {}; + // The context this container holds was set, replaced or cleared. Called + // after the change, and before any rebinding that follows it. + virtual void dataContextChanged() {}; + // Whether updateDataBinds would process anything: a bind waiting in any + // queue, or one it polls on every pass. + bool hasDataBindWork() const; + // Whether advanceDataBinds could do anything: some bind's converter + // advances. + bool mayAdvanceDataBinds() const; +#ifdef TESTING + // Binds processed by updateDataBinds, across every container. + static uint64_t sm_dataBindUpdates; +#endif // Runs once an update pass has drained its deferred adds and removes. virtual void dataBindsProcessed() {}; virtual void rebuildDataBind(DataBind*) {};
diff --git a/include/rive/layout/layout_participant.hpp b/include/rive/layout/layout_participant.hpp index e376a4f..303c783 100644 --- a/include/rive/layout/layout_participant.hpp +++ b/include/rive/layout/layout_participant.hpp
@@ -66,6 +66,7 @@ bool advanceComponent(float elapsedSeconds, AdvanceFlags flags = AdvanceFlags::Animate | AdvanceFlags::NewFrame) override; + QuietState quietState() override; #ifdef WITH_RIVE_LAYOUT bool cascadeLayoutStyle(LayoutStyleInterpolation inheritedInterpolation, KeyFrameInterpolator* inheritedInterpolator,
diff --git a/include/rive/layout_component.hpp b/include/rive/layout_component.hpp index 1120ad1..f94f2cd 100644 --- a/include/rive/layout_component.hpp +++ b/include/rive/layout_component.hpp
@@ -512,6 +512,7 @@ bool advanceComponent(float elapsedSeconds, AdvanceFlags flags = AdvanceFlags::Animate | AdvanceFlags::NewFrame) override; + QuietState quietState() override; bool isHidden() const override; float forcedWidth() { return m_forcedWidth; } float forcedHeight() { return m_forcedHeight; }
diff --git a/include/rive/scripted/scripted_data_converter.hpp b/include/rive/scripted/scripted_data_converter.hpp index adc8bce..fe78d52 100644 --- a/include/rive/scripted/scripted_data_converter.hpp +++ b/include/rive/scripted/scripted_data_converter.hpp
@@ -49,6 +49,7 @@ AdvanceFlags flags = AdvanceFlags::Animate | AdvanceFlags::NewFrame) override; bool advance(float elapsedSeconds) override; + bool mayAdvance() const override { return true; } StatusCode import(ImportStack& importStack) override; void addProperty(CustomProperty* prop) override; Core* clone() const override;
diff --git a/src/animation/state_machine.cpp b/src/animation/state_machine.cpp index 4025e2d..2d468b5 100644 --- a/src/animation/state_machine.cpp +++ b/src/animation/state_machine.cpp
@@ -6,9 +6,164 @@ #include "rive/animation/state_machine_listener.hpp" #include "rive/scripted/scripted_object.hpp" #include "rive/data_bind/data_bind.hpp" +#ifndef WITH_RIVE_EDITOR +#include "rive/animation/layer_state.hpp" +#include "rive/animation/layer_state_flags.hpp" +#include "rive/animation/state_transition.hpp" +#include "rive/animation/transition_condition.hpp" +#include "rive/animation/transition_property_viewmodel_comparator.hpp" +#include "rive/animation/transition_self_comparator.hpp" +#include "rive/animation/transition_value_artboard_comparator.hpp" +#include "rive/animation/transition_value_asset_comparator.hpp" +#include "rive/animation/transition_value_boolean_comparator.hpp" +#include "rive/animation/transition_value_color_comparator.hpp" +#include "rive/animation/transition_value_enum_comparator.hpp" +#include "rive/animation/transition_value_number_comparator.hpp" +#include "rive/animation/transition_value_string_comparator.hpp" +#include "rive/animation/transition_value_trigger_comparator.hpp" +#include "rive/animation/transition_viewmodel_condition.hpp" +#include "rive/data_bind/bindable_property_boolean.hpp" +#include "rive/data_bind/bindable_property_color.hpp" +#include "rive/data_bind/bindable_property_enum.hpp" +#include "rive/data_bind/bindable_property_integer.hpp" +#include "rive/data_bind/bindable_property_number.hpp" +#include "rive/data_bind/bindable_property_string.hpp" +#include "rive/data_bind/bindable_property_trigger.hpp" +#include "rive/data_bind_flags.hpp" +#include <unordered_set> +#endif using namespace rive; +#ifndef WITH_RIVE_EDITOR +// Settle-safety, see LayerState::transitionsSettleSafe. A comparator is tracked +// when every value it reads can only change through a dirty event on one of the +// instance's own data binds, or a change of its data context: both unsettle the +// instance's layers. Exact type keys on purpose, so a new condition or +// comparator type starts out untracked (always searched) rather than +// inheriting its parent's answer. `untracked` holds the objects that state +// machine binds write without going through that path. +static bool isTrackedComparator( + TransitionComparator* comparator, + const std::unordered_set<const Core*>& untracked) +{ + if (comparator == nullptr) + { + // A missing side compares as ConditionComparisonNone: always false. + return true; + } + if (untracked.count(comparator) != 0) + { + return false; + } + switch (comparator->coreType()) + { + case TransitionPropertyViewModelComparatorBase::typeKey: + { + auto bindable = + comparator->as<TransitionPropertyViewModelComparator>() + ->bindableProperty(); + if (bindable == nullptr || untracked.count(bindable) != 0) + { + return false; + } + switch (bindable->coreType()) + { + case BindablePropertyNumberBase::typeKey: + case BindablePropertyIntegerBase::typeKey: + case BindablePropertyBooleanBase::typeKey: + case BindablePropertyStringBase::typeKey: + case BindablePropertyColorBase::typeKey: + case BindablePropertyEnumBase::typeKey: + case BindablePropertyTriggerBase::typeKey: + return true; + default: + // Asset, artboard and view model bindables are also written + // outside their binds' dirty path. + return false; + } + } + case TransitionValueNumberComparatorBase::typeKey: + case TransitionValueBooleanComparatorBase::typeKey: + case TransitionValueStringComparatorBase::typeKey: + case TransitionValueColorComparatorBase::typeKey: + case TransitionValueEnumComparatorBase::typeKey: + case TransitionValueTriggerComparatorBase::typeKey: + case TransitionValueAssetComparatorBase::typeKey: + case TransitionValueArtboardComparatorBase::typeKey: + case TransitionSelfComparatorBase::typeKey: + return true; + default: + // Component and artboard property comparators read live values + // that nothing reports to the state machine. + return false; + } +} + +static bool isTrackedCondition(TransitionCondition* condition, + const std::unordered_set<const Core*>& untracked) +{ + // Only view model conditions are tracked. State machine inputs, focus, + // artboard and scripted conditions keep the full search. + if (condition == nullptr || + condition->coreType() != TransitionViewModelConditionBase::typeKey) + { + return false; + } + auto viewModelCondition = condition->as<TransitionViewModelCondition>(); + return isTrackedComparator(viewModelCondition->leftComparator(), + untracked) && + isTrackedComparator(viewModelCondition->rightComparator(), + untracked); +} + +static bool isSettleSafe(const LayerState* state, + const std::unordered_set<const Core*>& untracked) +{ + // A random state can allow transitions and still take none (a draw of + // exactly 1.0 falls through the weights), and must be searched again. + if ((static_cast<LayerStateFlags>(state->flags()) & + LayerStateFlags::Random) == LayerStateFlags::Random) + { + return false; + } + for (size_t i = 0, count = state->transitionCount(); i < count; i++) + { + auto transition = state->transition(i); + if (transition == nullptr || transition->isDisabled()) + { + continue; + } + for (size_t j = 0, conditions = transition->conditionCount(); + j < conditions; + j++) + { + if (!isTrackedCondition(transition->condition(j), untracked)) + { + return false; + } + } + } + return true; +} + +// Only the flags are read here, since a layer's animations and target states +// may not be resolved yet when this runs (see onAddedClean). +static bool transitionsIgnoreTime(const LayerState* state) +{ + for (size_t i = 0, count = state->transitionCount(); i < count; i++) + { + auto transition = state->transition(i); + if (transition != nullptr && !transition->isDisabled() && + transition->enableExitTime()) + { + return false; + } + } + return true; +} +#endif + StateMachine::StateMachine() {} StateMachine::~StateMachine() {} @@ -64,6 +219,39 @@ return code; } } +#ifndef WITH_RIVE_EDITOR + // Classified here, once the whole artboard has been read, from what + // reading attaches: conditions, comparators, bindables and flags. Importers + // resolve later still (a layer's only when the next layer arrives, which + // can be in the next artboard), so nothing they fill in, like a state's + // animation, is read here. Bindables fed once never report later source + // changes, and binds aimed at anything but a bindable write the shared + // definition (a comparator's constant, say) directly. + std::unordered_set<const Core*> untracked; + for (auto& dataBind : m_dataBinds) + { + auto target = dataBind->target(); + bool bindsOnce = + enums::is_flag_set(static_cast<DataBindFlags>(dataBind->flags()), + DataBindFlags::Once); + if (target != nullptr && (bindsOnce || !target->is<BindableProperty>())) + { + untracked.insert(target); + } + } + for (auto& layer : m_Layers) + { + for (size_t i = 0, count = layer->stateCount(); i < count; i++) + { + auto state = layer->state(i); + if (state != nullptr) + { + state->transitionsSettleSafe(isSettleSafe(state, untracked)); + state->transitionsIgnoreTime(transitionsIgnoreTime(state)); + } + } + } +#endif return StatusCode::Ok; }
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 9ede6c4..a81c5c4 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp
@@ -132,6 +132,14 @@ class StateMachineLayerInstance { public: + StateMachineLayerInstance() : + m_transitionCompleted(false), + m_holdAnimationFrom(false), + m_stateMachineChangedOnAdvance(false), + m_waitingForExit(false), + m_settled(false) + {} + ~StateMachineLayerInstance() { delete m_anyStateInstance; @@ -170,6 +178,9 @@ void resetState(StateMachineInstance* smi) { + // The current state goes away below even when changeState then finds + // nothing to change to (no entry state). + m_settled = false; if (m_stateFrom != m_anyStateInstance && m_stateFrom != m_currentState) { delete m_stateFrom; @@ -216,12 +227,57 @@ } } + // A settled layer that has finished mixing, in a state that keys nothing, + // has stopped keeping itself going and has no exit time to wait on: its + // advance would only move a clock nothing reads, apply nothing and skip a + // search that can't pass, so it returns what that advance would have. + bool isFrozen() const + { + if (!m_settled || m_mix != 1.0f || m_animationReset != nullptr || + m_holdAnimation != nullptr || + (m_transition != nullptr && !m_transitionCompleted) || + m_currentState == nullptr || m_currentState->keepGoing()) + { + return false; + } + auto state = m_currentState->state(); + if (!state->transitionsIgnoreTime()) + { + return false; + } + // Checked on the instance rather than classified with the state: a + // state's animation may not be resolved yet when that runs. + switch (state->coreType()) + { + case EntryStateBase::typeKey: + case ExitStateBase::typeKey: + case AnyStateBase::typeKey: + return true; + case AnimationStateBase::typeKey: + // An animation state without an animation plays an empty one. + return static_cast<const AnimationStateInstance*>( + m_currentState) + ->animationInstance() + ->animation() + ->numKeyedObjects() == 0; + default: + return false; + } + } + bool advance(StateMachineInstance* smi, float seconds, bool newFrame) { if (newFrame) { m_stateMachineChangedOnAdvance = false; } + if (isFrozen()) + { +#ifdef TESTING + StateMachineInstance::sm_frozenLayerAdvances++; +#endif + return false; + } m_currentState->advance(seconds, smi); updateMix(smi, seconds); @@ -349,14 +405,82 @@ m_waitingForExit = false; ensureAnyStateInstance(smi); + if (m_settled) + { +#ifdef TESTING + verifySettled(smi); + StateMachineInstance::sm_settledLayerSkips++; +#endif + return false; + } if (tryChangeState(smi, m_anyStateInstance)) { return true; } - - return tryChangeState(smi, m_currentState); + if (tryChangeState(smi, m_currentState)) + { + return true; + } +#ifndef WITH_RIVE_EDITOR + // Nothing to change to, and that holds until the current state changes + // or a value these transitions read does, provided they read only + // values whose changes reach this instance (transitionsSettleSafe), + // none is waiting on an exit time, and no bind is waiting to be + // applied (a condition may just have read a bindable's old value). + // StateMachineInstance::unsettleLayers undoes it. + if (!m_waitingForExit && !smi->m_bindsPending && + isSettleSafe(m_anyStateInstance) && isSettleSafe(m_currentState)) + { + m_settled = true; + smi->m_anySettled = true; + } +#endif + return false; } + void unsettle() { m_settled = false; } + + static bool isSettleSafe(const StateInstance* stateInstance) + { + return stateInstance == nullptr || + stateInstance->state()->transitionsSettleSafe(); + } + +#ifdef TESTING + // A settled layer's search must still come up empty. Repeats it without + // its side effects (random weights, used-layer marks, m_waitingForExit) + // and stops loudly if the settled answer went stale. + void verifySettled(StateMachineInstance* smi) + { + StateInstance* stateFromInstances[] = {m_anyStateInstance, + m_currentState}; + for (StateInstance* stateFromInstance : stateFromInstances) + { + if (stateFromInstance == nullptr) + { + continue; + } + auto stateFrom = stateFromInstance->state(); + for (size_t i = 0, count = stateFrom->transitionCount(); i < count; + i++) + { + auto transition = stateFrom->transition(i); + if (canChangeState(transition->stateTo()) && + transition->allowed(stateFromInstance, smi, this) != + AllowTransition::no) + { + fprintf(stderr, + "Settled layer %s on artboard %s would have taken " + "a transition\n", + m_layer->name().c_str(), + smi->artboard()->name().c_str()); + abort(); + } + } + } + } +#endif + void fireEvents(StateMachineInstance* smi, StateMachineFireOccurance occurs, const std::vector<StateMachineFireAction*>& fireEvents) @@ -400,6 +524,8 @@ { return; } + // A different state has different transitions to search. + m_settled = false; // Fire end events for the state we're changing from. if (m_currentState != nullptr) @@ -730,10 +856,16 @@ float m_mixFrom = 1.0f; float m_holdTime = 0.0f; - bool m_transitionCompleted = false; - bool m_holdAnimationFrom = false; - bool m_stateMachineChangedOnAdvance = false; - bool m_waitingForExit = false; + // One bit each, so m_settled shares the byte the four flags before it + // already take and sizeof stays 80. Bit-fields can't have default member + // initializers before C++20, so the constructor sets them. + bool m_transitionCompleted : 1; + bool m_holdAnimationFrom : 1; + bool m_stateMachineChangedOnAdvance : 1; + bool m_waitingForExit : 1; + // The last search found nothing and nothing it read has changed since, + // so the next one would find nothing too. See updateState. + bool m_settled : 1; }; /// Representation of a Component from the Artboard Instance and all the @@ -2405,6 +2537,9 @@ } #ifdef TESTING +uint64_t StateMachineInstance::sm_settledLayerSkips = 0; +uint64_t StateMachineInstance::sm_frozenLayerAdvances = 0; + const LayerState* StateMachineInstance::layerState(size_t index) { if (index < m_machine->layerCount()) @@ -2505,7 +2640,11 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine, ArtboardInstance* instance) : - Scene(instance), m_machine(machine) + Scene(instance), + m_machine(machine), + m_bindsPending(false), + m_anySettled(false), + m_destroying(false) { const auto count = machine->inputCount(); m_inputInstances.resize(count); @@ -2584,7 +2723,6 @@ dataBindClone->converter( dataBind->converter()->clone()->as<DataConverter>()); } - addDataBind(dataBindClone); if (dataBind->target()->is<BindableProperty>()) { auto& bindables = ensureBindables(); @@ -2634,6 +2772,10 @@ BindablePropertyNumberBase::propertyValuePropertyKey); } } + // Added once its target is set: addDataBind decides from the target + // whether a toSource bind hears about changes or has to poll for them, + // and without one it would poll on every pass. + addDataBind(dataBindClone); } // Initialize listeners. Store a lookup table of shape id to hit shape @@ -2999,6 +3141,7 @@ StateMachineInstance::~StateMachineInstance() { + m_destroying = true; // Clean up semantic tree BEFORE the internal SemanticManager is destroyed. // Only needed when we own the manager; if external, the parent cleans up. @@ -3023,6 +3166,9 @@ } deleteDataBinds(); delete[] m_layers; + // What's torn down below can still dirty a bind, which unsettles layers. + m_layers = nullptr; + m_anySettled = false; // The bindable clones and per-transition property instances are raw-owning, // so they are deleted here rather than by the cluster's destructor. if (auto* bindables = m_bindables.get()) @@ -3282,7 +3428,7 @@ bool isFocus) { ensureInputExtras().queuedFocusEvents.push_back({group, isFocus}); - m_needsAdvance = true; + markNeedsAdvance(); } void StateMachineInstance::setFocus(FocusData* focusData) @@ -3347,7 +3493,7 @@ return; } focusManager()->requestFocus(focusData->focusNode(), rootArtboard()); - m_needsAdvance = true; + markNeedsAdvance(); } void StateMachineInstance::queueClearFocus() @@ -3357,7 +3503,7 @@ return; } focusManager()->requestClearFocus(rootArtboard()); - m_needsAdvance = true; + markNeedsAdvance(); } void StateMachineInstance::queueFocusTraversal(uint32_t traversalKind) @@ -3367,7 +3513,7 @@ return; } focusManager()->requestTraversal(traversalKind, rootArtboard()); - m_needsAdvance = true; + markNeedsAdvance(); } void StateMachineInstance::processFocusEvents() @@ -3403,7 +3549,7 @@ SemanticActionType actionType) { ensureInputExtras().queuedSemanticEvents.push_back({group, actionType}); - m_needsAdvance = true; + markNeedsAdvance(); } void StateMachineInstance::processSemanticEvents() @@ -3621,11 +3767,16 @@ return keepGoing || hasPendingReports(); } -void StateMachineInstance::markNeedsAdvance() { m_needsAdvance = true; } +void StateMachineInstance::markNeedsAdvance() +{ + m_needsAdvance = true; + wakeRow(); +} bool StateMachineInstance::needsAdvance() const { return m_needsAdvance; } void StateMachineInstance::resetState() { + wakeRow(); for (size_t i = 0; i < m_layerCount; i++) { m_layers[i].resetState(this); @@ -3912,12 +4063,110 @@ void StateMachineInstance::mainViewModelInstanceChanged() { + unsettleLayers(); if (m_artboardInstance != nullptr) { m_artboardInstance->mainViewModelInstanceChanged(); } } +void StateMachineInstance::unsettleLayers() +{ + // Whatever unsettles a layer can change a quiet row too. + wakeRow(); + if (!m_anySettled) + { + return; + } + m_anySettled = false; + for (uint32_t i = 0; i < m_layerCount; i++) + { + m_layers[i].unsettle(); + } +} + +void StateMachineInstance::wakeRow() +{ + if (m_artboardInstance != nullptr && !m_destroying) + { + m_artboardInstance->wakeIfQuietRow(); + } +} + +AdvancingComponent::QuietState StateMachineInstance::rowQuietState() +{ + using QuietState = AdvancingComponent::QuietState; + // Scripts do work this can't see into. + if (m_scripting.get() != nullptr) + { + return QuietState::never; + } + // Cheapest first: a busy row is checked again every frame. + if (m_needsAdvance || m_bindsPending || hasDataBindWork() || + m_drawOrderChangeCounter != + m_artboardInstance->drawOrderChangeCounter()) + { + return QuietState::busy; + } + if (auto* reporting = this->reporting()) + { + if (!reporting->reportedEvents.empty() || + !reporting->reportingEvents.empty() || + !reporting->eventsAppliedDuringLoop.empty() || + !reporting->reportedListenerViewModels.empty() || + !reporting->reportingListenerViewModels.empty()) + { + return QuietState::busy; + } + } + if (auto* extras = inputExtras()) + { + if (!extras->queuedFocusEvents.empty() || + !extras->queuedSemanticEvents.empty()) + { + return QuietState::busy; + } + } + // Every layer frozen, and none changed state last frame (a new frame's + // advance clears that). + for (uint32_t i = 0; i < m_layerCount; i++) + { + if (!m_layers[i].isFrozen() || m_layers[i].stateChangedOnAdvance()) + { + return QuietState::busy; + } + } + // Converters that advance can't say whether they would. + return mayAdvanceDataBinds() ? QuietState::never : QuietState::quiet; +} + +void StateMachineInstance::addDirtyDataBind(DataBind* dataBind) +{ + // A bound value is about to change. Layers that settled on the old one + // must search again, and none may settle until it has been applied. Set + // before the base, which skips binds already queued or polled. + m_bindsPending = true; + unsettleLayers(); + DataBindContainer::addDirtyDataBind(dataBind); +} + +void StateMachineInstance::updateDataBinds(bool applyTargetToSource) +{ + // A nested call returns without applying anything, so only the outer pass + // clears this; binds dirtied while it runs set it again. + if (!isProcessingDataBinds()) + { + m_bindsPending = false; + } + DataBindContainer::updateDataBinds(applyTargetToSource); +} + +void StateMachineInstance::dataContextChanged() +{ + // View model conditions only evaluate with a data context. + unsettleLayers(); +} + void StateMachineInstance::dropInstanceValueBindsTargeting(Core* target) { DataBindContainer::dropInstanceValueBindsTargeting(target); @@ -3929,6 +4178,7 @@ void StateMachineInstance::relinkDataContext() { + unsettleLayers(); m_artboardInstance->relinkDataContext(); } @@ -4018,6 +4268,7 @@ void StateMachineInstance::reportEvent(Event* event, float delaySeconds) { + wakeRow(); ensureReporting().reportedEvents.push_back( EventReport(event, delaySeconds)); } @@ -4025,6 +4276,7 @@ void StateMachineInstance::reportListenerViewModel( ListenerViewModel* listenerViewModel) { + wakeRow(); ensureReporting().reportedListenerViewModels.push_back(listenerViewModel); }
diff --git a/src/animation/state_machine_listener.cpp b/src/animation/state_machine_listener.cpp index a31086f..5fddcdc 100644 --- a/src/animation/state_machine_listener.cpp +++ b/src/animation/state_machine_listener.cpp
@@ -150,6 +150,9 @@ StateMachineInstance* stateMachineInstance, const ListenerInvocation& invocation) const { + // Every listener kind acts through here, including keyboard and text ones + // that mark nothing else, so a quiet list row wakes up to what they do. + stateMachineInstance->wakeRow(); for (auto& action : m_actions) { action->perform(stateMachineInstance, invocation);
diff --git a/src/artboard.cpp b/src/artboard.cpp index 1b0ae5a..9f6b17f 100644 --- a/src/artboard.cpp +++ b/src/artboard.cpp
@@ -94,6 +94,7 @@ uint64_t Artboard::sm_frameId = 0; #ifdef TESTING uint64_t Artboard::sm_layoutPassCount = 0; +uint64_t Artboard::sm_dirtNotifications = 0; #endif Artboard::Artboard() @@ -1516,6 +1517,10 @@ void Artboard::onComponentDirty(Component* component) { +#ifdef TESTING + sm_dirtNotifications++; +#endif + wakeIfQuietRow(); m_didChange = true; m_Dirt |= ComponentDirt::Components; @@ -1530,6 +1535,10 @@ void Artboard::onDirty(ComponentDirt dirt) { +#ifdef TESTING + sm_dirtNotifications++; +#endif + wakeIfQuietRow(); m_Dirt |= ComponentDirt::Components; } @@ -1607,7 +1616,62 @@ } } -void Artboard::mainViewModelInstanceChanged() { syncInstanceValueBinds(); } +void Artboard::mainViewModelInstanceChanged() +{ + wakeIfQuietRow(); + syncInstanceValueBinds(); +} + +void Artboard::dataContextChanged() { wakeIfQuietRow(); } + +void Artboard::wakeQuietRow() +{ + auto row = m_quietHostRow; + m_quietHostRow = kNoQuietRow; + if (m_host != nullptr) + { + m_host->hostedRowWoke(this, row); + } +} + +AdvancingComponent::QuietState Artboard::rowQuietState() +{ + using QuietState = AdvancingComponent::QuietState; + // Work this check can't see into: hosted artboards and lists, joysticks, + // resettables and scripts. + if (!m_ArtboardHosts.empty() || !m_Joysticks.empty() || + !m_Resettables.empty() || !m_ScriptedObjects.empty()) + { + return QuietState::never; + } + // Cheapest first: a busy row is checked again every frame. + if (hasDirt(ComponentDirt::Components) || !m_dirtyLayout.empty() || + m_hostTransformMarkedDirty || m_instanceValueBindsPending || + hasDataBindWork()) + { + return QuietState::busy; + } + auto state = QuietState::quiet; + for (auto advancing : m_advancingComponents) + { + switch (advancing->quietState()) + { + case QuietState::never: + return QuietState::never; + case QuietState::busy: + state = QuietState::busy; + break; + case QuietState::quiet: + break; + } + } + if (state == QuietState::quiet && mayAdvanceDataBinds()) + { + // Converters that advance can't say whether they would. + return QuietState::never; + } + return state; +} void Artboard::dataBindsProcessed() { @@ -1784,6 +1848,7 @@ void Artboard::addDirtyDataBind(DataBind* dataBind) { + wakeIfQuietRow(); // Most artboard data binds target Components and need the component graph // marked dirty. Keyframe value binds instead target transient // BindableProperty holders (see @@ -1891,6 +1956,7 @@ void Artboard::markLayoutDirty(LayoutComponent* layoutComponent) { + wakeIfQuietRow(); assert(!m_isCleaningDirtyLayouts); if (m_isCleaningDirtyLayouts) { @@ -1922,6 +1988,7 @@ void Artboard::markHostTransformDirty() { + wakeIfQuietRow(); #ifdef WITH_RIVE_TOOLS if (!m_hostTransformMarkedDirty && m_transformDirtyCallback != nullptr) { @@ -4196,6 +4263,7 @@ void Artboard::relinkDataContext() { + wakeIfQuietRow(); if (dataBindContext() == nullptr) { return;
diff --git a/src/artboard_component_list.cpp b/src/artboard_component_list.cpp index 7365d99..32a7fb4 100644 --- a/src/artboard_component_list.cpp +++ b/src/artboard_component_list.cpp
@@ -9,6 +9,8 @@ #include "rive/constraints/list_constraint.hpp" #include "rive/constraints/scrolling/scroll_constraint.hpp" #include "rive/data_bind/data_bind.hpp" +#include "rive/data_bind/data_context.hpp" +#include "rive/math/bitwise.hpp" #include "rive/data_bind_flags.hpp" #include "rive/generated/viewmodel/viewmodel_instance_number_base.hpp" #include "rive/generated/viewmodel/viewmodel_instance_string_base.hpp" @@ -119,6 +121,8 @@ void ArtboardComponentList::clear() { + // Rows being destroyed must not call back into quiet-row bookkeeping. + resetQuietRows(0); // Clean up semantic trees before destroying artboards/state machines. for (auto& artboard : m_artboardInstancesMap) { @@ -162,8 +166,10 @@ m_listItems.clear(); m_listHasDuplicateItems = false; m_artboardsMap.clear(); - m_resourcePool.clear(); + // Pooled state machines go before the pooled artboards too, for the same + // reason as above. m_stateMachinesPool.clear(); + m_resourcePool.clear(); m_artboardOverridesMap.clear(); } @@ -221,6 +227,7 @@ if (index >= 0 && static_cast<size_t>(index) < m_listItems.size() && m_listItems[index] == item) { + resetQuietRow(index); m_artboardInstancesByIndex[index] = artboard; m_stateMachinesByIndex[index] = stateMachine; } @@ -230,6 +237,7 @@ { if (m_listItems[i] == item) { + resetQuietRow(i); m_artboardInstancesByIndex[i] = artboard; m_stateMachinesByIndex[i] = stateMachine; } @@ -820,6 +828,7 @@ m_artboardSizes.clear(); // Clear the index vectors - they'll be rebuilt as artboards are created + resetQuietRows(m_listItems.size()); m_artboardInstancesByIndex.assign(m_listItems.size(), nullptr); m_stateMachinesByIndex.assign(m_listItems.size(), nullptr); @@ -897,6 +906,270 @@ } } +#ifdef TESTING +uint64_t ArtboardComponentList::sm_quietRowSkips = 0; +bool ArtboardComponentList::sm_quietRowsEnabled = true; +#endif + +static size_t lowestSetBit(uint64_t bits) +{ + return static_cast<size_t>(63 - math::clz64(bits & (~bits + 1))); +} + +size_t ArtboardComponentList::nextAwakeRow(size_t row) const +{ + const size_t count = m_listItems.size(); + while (row < count) + { + const size_t word = row >> 6; + const uint64_t quiet = + word < m_quietRows.size() ? m_quietRows[word] : 0; + const uint64_t awake = ~quiet & (~uint64_t(0) << (row & 63)); + if (awake != 0) + { + return std::min(count, (word << 6) + lowestSetBit(awake)); + } + row = (word + 1) << 6; + } + return count; +} + +bool ArtboardComponentList::isRowQuiet(size_t row) const +{ + const size_t word = row >> 6; + return word < m_quietRows.size() && + ((m_quietRows[word] >> (row & 63)) & 1) != 0; +} + +AdvancingComponent::QuietState ArtboardComponentList::rowQuietState(size_t row) +{ + using QuietState = AdvancingComponent::QuietState; + auto artboard = m_artboardInstancesByIndex[row]; + auto stateMachine = m_stateMachinesByIndex[row]; + if (artboard == nullptr) + { + // An unrealized row has no work at all until it is realized, which + // resets it (setRowsForItem). + return stateMachine == nullptr ? QuietState::quiet : QuietState::busy; + } + // Pooled machines pair up with pooled artboards by pool order only. + if (stateMachine != nullptr && stateMachine->artboard() != artboard) + { + return QuietState::busy; + } + if (m_shouldResetInstances) + { + // reset() also advances a bound main instance that isn't the item's. + auto dataContext = artboard->dataContext(); + if (dataContext != nullptr) + { + auto bound = dataContext->mainViewModelInstance(); + if (bound != nullptr && + bound.get() != m_listItems[row]->viewModelInstance().get()) + { + return QuietState::busy; + } + } + } + auto state = artboard->rowQuietState(); + if (state != QuietState::quiet || stateMachine == nullptr) + { + return state; + } + return stateMachine->rowQuietState(); +} + +bool ArtboardComponentList::tryQuietRow(size_t row) +{ + const size_t word = row >> 6; + const uint64_t bit = uint64_t(1) << (row & 63); + // A row sharing its item's instances with other rows can't be judged on + // its own. + if (m_listHasDuplicateItems || word >= m_quietRows.size() || + (m_neverQuietRows[word] & bit) != 0) + { + return false; + } +#ifdef TESTING + if (!sm_quietRowsEnabled) + { + return false; + } +#endif + switch (rowQuietState(row)) + { + case AdvancingComponent::QuietState::never: + m_neverQuietRows[word] |= bit; + return false; + case AdvancingComponent::QuietState::busy: + return false; + case AdvancingComponent::QuietState::quiet: + break; + } + m_quietRows[word] |= bit; + if (auto artboard = m_artboardInstancesByIndex[row]) + { + artboard->quietHostRow(static_cast<uint32_t>(row)); + } + return true; +} + +void ArtboardComponentList::resetQuietRow(size_t row) +{ + const size_t word = row >> 6; + if (word >= m_quietRows.size()) + { + return; + } + const uint64_t bit = uint64_t(1) << (row & 63); + if ((m_quietRows[word] & bit) != 0 && + row < m_artboardInstancesByIndex.size() && + m_artboardInstancesByIndex[row] != nullptr) + { + m_artboardInstancesByIndex[row]->quietHostRow(Artboard::kNoQuietRow); + } + m_quietRows[word] &= ~bit; + m_neverQuietRows[word] &= ~bit; +} + +void ArtboardComponentList::resetQuietRows(size_t rowCount) +{ + for (size_t word = 0; word < m_quietRows.size(); word++) + { + for (uint64_t quiet = m_quietRows[word]; quiet != 0; quiet &= quiet - 1) + { + const size_t row = (word << 6) + lowestSetBit(quiet); + if (row < m_artboardInstancesByIndex.size() && + m_artboardInstancesByIndex[row] != nullptr) + { + m_artboardInstancesByIndex[row]->quietHostRow( + Artboard::kNoQuietRow); + } + } + } + const size_t words = (rowCount + 63) >> 6; + m_quietRows.assign(words, 0); + m_neverQuietRows.assign(words, 0); +} + +void ArtboardComponentList::hostedRowWoke(Artboard* artboard, uint32_t row) +{ + // Rows are reset before their instances change, so the artboard is still + // the row's; checked anyway, since waking a row by mistake only costs a + // frame of its work. + if (row < m_artboardInstancesByIndex.size() && + m_artboardInstancesByIndex[row] == artboard) + { + const size_t word = row >> 6; + if (word < m_quietRows.size()) + { + m_quietRows[word] &= ~(uint64_t(1) << (row & 63)); + } + } +} + +void ArtboardComponentList::shouldResetInstances(bool value) +{ + if (value != m_shouldResetInstances) + { + // Rows were judged quiet with the other setting's reset work. + resetQuietRows(m_listItems.size()); + } + m_shouldResetInstances = value; +} + +#ifdef TESTING +void ArtboardComponentList::verifyQuietRows(RowPass pass, + float elapsedSeconds, + AdvanceFlags flags, + bool advanceNested) +{ + for (size_t word = 0; word < m_quietRows.size(); word++) + { + for (uint64_t quiet = m_quietRows[word]; quiet != 0; quiet &= quiet - 1) + { + const size_t row = (word << 6) + lowestSetBit(quiet); + sm_quietRowSkips++; + auto artboard = m_artboardInstancesByIndex[row]; + auto stateMachine = m_stateMachinesByIndex[row]; + const char* problem = nullptr; + if (rowQuietState(row) != AdvancingComponent::QuietState::quiet) + { + problem = "stopped being quiet but nothing woke it"; + } + else if (artboard == nullptr) + { + // Unrealized, so there is no work to check. + } + else if (artboard->quietHostRow() != row) + { + problem = "lost track of its artboard"; + } + else + { + auto dirt = Artboard::sm_dirtNotifications; + auto updates = DataBindContainer::sm_dataBindUpdates; + bool didWork = false; + switch (pass) + { + case RowPass::advance: + if (advanceNested && stateMachine != nullptr) + { + didWork = + stateMachine->advance(elapsedSeconds, true); + } + didWork = + artboard->advanceInternal(elapsedSeconds, flags) || + didWork; + break; + case RowPass::settle: + if (advanceNested && stateMachine != nullptr) + { + didWork = stateMachine->tryChangeState(); + } + didWork = + artboard->advanceInternal(elapsedSeconds, flags) || + didWork; + break; + case RowPass::updateDataBinds: + if (stateMachine != nullptr) + { + stateMachine->updateDataBinds(false); + } + artboard->updateDataBinds(); + break; + case RowPass::reset: + artboard->reset(); + break; + case RowPass::update: + didWork = artboard->updatePass(false); + break; + } + if (didWork || dirt != Artboard::sm_dirtNotifications || + updates != DataBindContainer::sm_dataBindUpdates || + artboard->hasDirt(ComponentDirt::Components) || + artboard->quietHostRow() != row || + (stateMachine != nullptr && + (stateMachine->needsAdvance() || + stateMachine->stateChangedCount() != 0))) + { + problem = "did work while quiet"; + } + } + if (problem != nullptr) + { + fprintf(stderr, + "Quiet row %zu of list '%s' %s\n", + row, + name().c_str(), + problem); + abort(); + } + } + } +} +#endif + bool ArtboardComponentList::advanceComponent(float elapsedSeconds, AdvanceFlags flags) { @@ -909,8 +1182,16 @@ (flags & AdvanceFlags::AdvanceNested) == AdvanceFlags::AdvanceNested; bool newFrame = (flags & AdvanceFlags::NewFrame) == AdvanceFlags::NewFrame; auto advancingFlags = flags & ~AdvanceFlags::IsRoot; - for (int i = 0; i < artboardCount(); i++) + const size_t count = m_listItems.size(); + for (size_t row = nextAwakeRow(0); row < count; row = nextAwakeRow(row + 1)) { + // A row whose work this frame would do nothing sleeps until something + // wakes it. + if (newFrame && tryQuietRow(row)) + { + continue; + } + int i = static_cast<int>(row); if (advanceNested) { auto stateMachine = stateMachineInstance(i); @@ -954,22 +1235,35 @@ } } } - +#ifdef TESTING + verifyQuietRows(newFrame ? RowPass::advance : RowPass::settle, + elapsedSeconds, + advancingFlags, + advanceNested); +#endif return keepGoing; } void ArtboardComponentList::reset() { - for (size_t i = 0; i < m_listItems.size(); i++) + const size_t count = m_listItems.size(); + // Detached item view models (see shouldResetInstances) are advanced on + // every row, quiet or not; otherwise only awake rows have work here. + for (size_t i = m_shouldResetInstances ? 0 : nextAwakeRow(0); i < count; + i = m_shouldResetInstances ? i + 1 : nextAwakeRow(i + 1)) { auto artboard = m_artboardInstancesByIndex[i]; if (m_shouldResetInstances) { - auto viewModelInstance = m_listItems[i]->viewModelInstance(); + auto& viewModelInstance = m_listItems[i]->viewModelInstance(); if (viewModelInstance != nullptr) { viewModelInstance->advanced(); } + if (isRowQuiet(i)) + { + continue; + } if (artboard != nullptr) { auto dataContext = artboard->dataContext(); @@ -989,6 +1283,9 @@ artboard->reset(); } } +#ifdef TESTING + verifyQuietRows(RowPass::reset, 0.0f, AdvanceFlags::None, false); +#endif } AABB ArtboardComponentList::layoutBounds() @@ -1360,14 +1657,19 @@ } if (hasDirt(value, ComponentDirt::Components)) { - for (int i = 0; i < artboardCount(); i++) + const size_t count = m_listItems.size(); + for (size_t row = nextAwakeRow(0); row < count; + row = nextAwakeRow(row + 1)) { - auto artboard = artboardInstance(i); + auto artboard = m_artboardInstancesByIndex[row]; if (artboard != nullptr) { artboard->updatePass(false); } } +#ifdef TESTING + verifyQuietRows(RowPass::update, 0.0f, AdvanceFlags::None, false); +#endif } } @@ -1477,19 +1779,23 @@ { return; } - for (int i = 0; i < artboardCount(); i++) + const size_t count = m_listItems.size(); + for (size_t row = nextAwakeRow(0); row < count; row = nextAwakeRow(row + 1)) { - auto stateMachine = stateMachineInstance(i); + auto stateMachine = m_stateMachinesByIndex[row]; if (stateMachine != nullptr) { stateMachine->updateDataBinds(false); } - auto artboard = artboardInstance(i); + auto artboard = m_artboardInstancesByIndex[row]; if (artboard != nullptr) { artboard->updateDataBinds(); } } +#ifdef TESTING + verifyQuietRows(RowPass::updateDataBinds, 0.0f, AdvanceFlags::None, false); +#endif } #ifdef WITH_RIVE_TOOLS @@ -1629,6 +1935,7 @@ { // updateList builds a non-virtualized list's rows in order and // gives an item's later rows the instances made for its first. + resetQuietRow(index); m_artboardInstancesByIndex[index] = artboardInstance; m_stateMachinesByIndex[index] = stateMachineInstance; }
diff --git a/src/data_bind/converters/data_converter_group.cpp b/src/data_bind/converters/data_converter_group.cpp index 0244d56..dab77c0 100644 --- a/src/data_bind/converters/data_converter_group.cpp +++ b/src/data_bind/converters/data_converter_group.cpp
@@ -114,6 +114,19 @@ } } +bool DataConverterGroup::mayAdvance() const +{ + for (auto& item : m_items) + { + auto converter = item->converter(); + if (converter != nullptr && converter->mayAdvance()) + { + return true; + } + } + return false; +} + bool DataConverterGroup::advance(float elapsedSeconds) { bool didUpdate = false;
diff --git a/src/data_bind/data_bind_container.cpp b/src/data_bind/data_bind_container.cpp index 643f391..2a4a479 100644 --- a/src/data_bind/data_bind_container.cpp +++ b/src/data_bind/data_bind_container.cpp
@@ -2,6 +2,7 @@ #include "rive/data_bind/data_bind_context.hpp" #include "rive/data_bind/data_bind.hpp" #include "rive/data_bind/data_context.hpp" +#include "rive/data_bind/converters/data_converter.hpp" #include <algorithm> using namespace rive; @@ -41,16 +42,19 @@ dataBind->unbind(); } m_dataContext = nullptr; + dataContextChanged(); } void DataBindContainer::dataBindContext(rcp<DataContext> dataContext) { m_dataContext = std::move(dataContext); + dataContextChanged(); } void DataBindContainer::bindDataBindsFromContext(rcp<DataContext> dataContext) { m_dataContext = std::move(dataContext); + dataContextChanged(); bindDataBindsFromContext(); } @@ -153,9 +157,45 @@ } } +#ifdef TESTING +uint64_t DataBindContainer::sm_dataBindUpdates = 0; +#endif + +bool DataBindContainer::hasDataBindWork() const +{ + if (!m_dirtyDataBinds.empty()) + { + return true; + } + auto* queues = m_queues.get(); + return queues != nullptr && + (!queues->persisting.empty() || !queues->dirtyToSource.empty() || + !queues->pendingDirtyToSource.empty() || + !queues->pendingDirty.empty() || + !queues->pendingAdditions.empty() || + !queues->pendingRemovals.empty() || + !queues->pendingDeletes.empty()); +} + +bool DataBindContainer::mayAdvanceDataBinds() const +{ + for (auto dataBind : m_dataBinds) + { + auto converter = dataBind->converter(); + if (converter != nullptr && converter->mayAdvance()) + { + return true; + } + } + return false; +} + void DataBindContainer::updateDataBind(DataBind* dataBind, bool applyTargetToSource) { +#ifdef TESTING + sm_dataBindUpdates++; +#endif auto d = dataBind->dirt(); // Update dependents before applying both target to source and source to
diff --git a/src/layout/layout_participant.cpp b/src/layout/layout_participant.cpp index fb4ca9b..00524ef 100644 --- a/src/layout/layout_participant.cpp +++ b/src/layout/layout_participant.cpp
@@ -542,6 +542,21 @@ return m_animation != nullptr ? m_animation->interpolator : nullptr; } +AdvancingComponent::QuietState LayoutParticipant::quietState() +{ +#ifdef WITH_RIVE_LAYOUT + // The early outs of advanceComponent and applyInterpolation. + if (m_animation == nullptr || + currentAnimationData()->to == m_animation->animatedLayout) + { + return QuietState::quiet; + } + return QuietState::busy; +#else + return QuietState::quiet; +#endif +} + bool LayoutParticipant::advanceComponent(float elapsedSeconds, AdvanceFlags flags) {
diff --git a/src/layout_component.cpp b/src/layout_component.cpp index cc3ecbc..9b682f7 100644 --- a/src/layout_component.cpp +++ b/src/layout_component.cpp
@@ -1485,6 +1485,18 @@ setLayoutFlag(LayoutComponentFlags::ForceUpdateLayoutBounds, false); } +AdvancingComponent::QuietState LayoutComponent::quietState() +{ + // The early outs of advanceComponent and applyInterpolation. + auto animationData = currentAnimationData(); + if (isCollapsed() || animationData == nullptr || !animates() || + m_style == nullptr || animationData->to == m_layout) + { + return QuietState::quiet; + } + return QuietState::busy; +} + bool LayoutComponent::advanceComponent(float elapsedSeconds, AdvanceFlags flags) { if ((flags & AdvanceFlags::NewFrame) != AdvanceFlags::NewFrame || @@ -1948,6 +1960,11 @@ return false; } +AdvancingComponent::QuietState LayoutComponent::quietState() +{ + return QuietState::quiet; +} + void LayoutComponent::markLayoutNodeDirty(bool shouldForceUpdateLayoutBounds) {} void LayoutComponent::markLayoutStyleDirty() {} void LayoutComponent::onDirty(ComponentDirt value) {}
diff --git a/tests/unit_tests/runtime/quiet_rows_test.cpp b/tests/unit_tests/runtime/quiet_rows_test.cpp new file mode 100644 index 0000000..87167aa --- /dev/null +++ b/tests/unit_tests/runtime/quiet_rows_test.cpp
@@ -0,0 +1,110 @@ +#include <rive/file.hpp> +#include <rive/artboard.hpp> +#include <rive/artboard_component_list.hpp> +#include <rive/animation/state_machine_instance.hpp> +#include <rive/viewmodel/viewmodel_instance.hpp> +#include "rive_file_reader.hpp" +#include <catch.hpp> +#include <utility> +#include <vector> + +// An ArtboardComponentList skips the per-frame work of rows that would do +// nothing ("quiet" rows) until something wakes them. Under TESTING every +// skipped row pass is still run, after the pass, and aborts if it did +// anything, so the whole suite checks the skipping; these cases pin down that +// rows do go quiet and that what the host pushes into them wakes them. + +using namespace rive; + +namespace +{ +struct RowLayout +{ + float width; + float height; + float minX; + float minY; +}; + +std::vector<RowLayout> rowLayouts(ArtboardComponentList* list) +{ + std::vector<RowLayout> rows; + for (size_t i = 0; i < list->artboardCount(); i++) + { + auto row = list->artboardInstance((int)i); + auto bounds = list->layoutBoundsForNode((int)i); + rows.push_back({row->layoutWidth(), + row->layoutHeight(), + bounds.minX, + bounds.minY}); + } + return rows; +} + +bool operator==(const RowLayout& a, const RowLayout& b) +{ + return a.width == b.width && a.height == b.height && a.minX == b.minX && + a.minY == b.minY; +} + +// Plays "Main" in artboard_list_overrides.riv until its rows could go quiet, +// halves the artboard's width, lets the layout settle, and returns where the +// rows were before and after, with quiet rows on or off. +std::pair<std::vector<RowLayout>, std::vector<RowLayout>> resizeHost( + File* file, + bool quietRows) +{ + ArtboardComponentList::sm_quietRowsEnabled = quietRows; + auto artboard = file->artboardNamed("Main"); + REQUIRE(artboard != nullptr); + auto stateMachine = artboard->stateMachineAt(0); + auto viewModelId = artboard->viewModelId(); + auto viewModelInstance = + viewModelId == -1 ? file->createViewModelInstance(artboard.get()) + : file->createViewModelInstance(viewModelId, 0); + stateMachine->bindViewModelInstance(viewModelInstance); + stateMachine->advanceAndApply(0.1f); + + auto lists = artboard->find<ArtboardComponentList>(); + REQUIRE(lists.size() == 1); + auto list = lists[0]; + REQUIRE(list->artboardCount() > 0); + + // Longer than the one-second empty animation a state without one plays, + // which keeps its layer going until it ends. + auto skips = ArtboardComponentList::sm_quietRowSkips; + for (int i = 0; i < 90; i++) + { + stateMachine->advanceAndApply(1.0f / 60.0f); + } + CHECK((ArtboardComponentList::sm_quietRowSkips > skips) == quietRows); + for (size_t i = 0; i < list->artboardCount(); i++) + { + CHECK((list->artboardInstance((int)i)->quietHostRow() == (uint32_t)i) == + quietRows); + } + + auto before = rowLayouts(list); + artboard->width(artboard->width() * 0.5f); + for (int i = 0; i < 60; i++) + { + stateMachine->advanceAndApply(1.0f / 60.0f); + } + + auto after = rowLayouts(list); + ArtboardComponentList::sm_quietRowsEnabled = true; + return {before, after}; +} +} // namespace + +TEST_CASE("Quiet list rows wake for layout the host pushes into them", + "[component_list]") +{ + auto file = ReadRiveFile("assets/artboard_list_overrides.riv"); + auto quiet = resizeHost(file.get(), true); + auto awake = resizeHost(file.get(), false); + // The resize must move the rows, or matching proves nothing. + CHECK_FALSE(quiet.first == quiet.second); + REQUIRE(quiet.second.size() == awake.second.size()); + CHECK(quiet.second == awake.second); +}
diff --git a/tests/unit_tests/runtime/settled_layers_test.cpp b/tests/unit_tests/runtime/settled_layers_test.cpp new file mode 100644 index 0000000..302e5d6 --- /dev/null +++ b/tests/unit_tests/runtime/settled_layers_test.cpp
@@ -0,0 +1,216 @@ +#include <rive/file.hpp> +#include <rive/animation/layer_state.hpp> +#include <rive/animation/state_machine_input_instance.hpp> +#include <rive/animation/state_machine_instance.hpp> +#include <rive/animation/state_machine_layer.hpp> +#include <rive/animation/state_transition.hpp> +#include <rive/shapes/shape.hpp> +#include <rive/shapes/paint/color.hpp> +#include <rive/shapes/paint/fill.hpp> +#include <rive/shapes/paint/solid_color.hpp> +#include <rive/viewmodel/viewmodel_instance_enum.hpp> +#include <rive/viewmodel/viewmodel_instance_trigger.hpp> +#include "rive_file_reader.hpp" +#include <catch.hpp> +#include <memory> + +// A layer whose transitions only read view model values stops searching them +// once a search finds nothing ("settles"), until a bound value, its state or +// the data context changes. Every settled search skipped under TESTING is also +// re-run and aborts the run if it would have found a transition, so the whole +// suite checks the cache; these cases pin down that it engages and wakes up. + +using namespace rive; + +static ColorInt fillColor(ArtboardInstance* artboard) +{ + auto shape = artboard->find<Shape>("color_rectangle"); + REQUIRE(shape != nullptr); + REQUIRE(shape->children()[1]->is<Fill>()); + return shape->children()[1] + ->as<Fill>() + ->paint() + ->as<SolidColor>() + ->colorValue(); +} + +TEST_CASE("View model conditions settle until a bound value changes", + "[state machine]") +{ + auto file = ReadRiveFile("assets/data_binding_test.riv"); + auto artboard = file->artboard("artboard-2")->instance(); + auto viewModelInstance = + file->createDefaultViewModelInstance(artboard.get()); + auto machine = artboard->defaultStateMachine(); + machine->bindViewModelInstance(viewModelInstance); + auto shape = artboard->find<Shape>("color_rectangle"); + auto state = + viewModelInstance->propertyValue("state")->as<ViewModelInstanceEnum>(); + auto trigger = viewModelInstance->propertyValue("trigger-prop") + ->as<ViewModelInstanceTrigger>(); + + machine->advanceAndApply(0.0f); + REQUIRE(fillColor(artboard.get()) == colorARGB(255, 255, 0, 0)); + + // Nothing changes, so the layers settle and stop searching. + auto skips = StateMachineInstance::sm_settledLayerSkips; + for (int i = 0; i < 10; i++) + { + machine->advanceAndApply(1.0f / 60.0f); + } + CHECK(StateMachineInstance::sm_settledLayerSkips > skips); + CHECK(fillColor(artboard.get()) == colorARGB(255, 255, 0, 0)); + + // A bound enum change wakes them on the very next advance. + state->value(1); + machine->advanceAndApply(0.0f); + CHECK(fillColor(artboard.get()) == colorARGB(255, 0, 255, 0)); + CHECK(shape->x() == 150); + CHECK(shape->y() == 250); + + for (int i = 0; i < 10; i++) + { + machine->advanceAndApply(1.0f / 60.0f); + } + + // So do an enum change and a trigger fired together on settled layers. + state->value("state-blue"); + trigger->propertyValue(1); + machine->advanceAndApply(0.0f); + CHECK(fillColor(artboard.get()) == colorARGB(255, 0, 0, 255)); + CHECK(shape->x() == 350); + CHECK(shape->y() == 250); + + for (int i = 0; i < 10; i++) + { + machine->advanceAndApply(1.0f / 60.0f); + } + + // And a trigger on its own. + trigger->propertyValue(1); + machine->advanceAndApply(0.0f); + CHECK(shape->x() == 350); + CHECK(shape->y() == 350); +} + +TEST_CASE("States reading state machine inputs are always searched", + "[state machine]") +{ + auto file = ReadRiveFile("assets/state_machine_triggers.riv"); + auto artboard = file->artboard("main"); + auto stateMachine = artboard->stateMachine("State Machine 1"); + + auto abi = artboard->instance(); + auto machine = + std::make_unique<StateMachineInstance>(stateMachine, abi.get()); + machine->advanceAndApply(0.1f); + auto idleState = machine->layerState(0); + REQUIRE(idleState != nullptr); + + // Its transitions wait on a trigger input, which takes the slow path: the + // layer never settles, however long nothing happens. + auto skips = StateMachineInstance::sm_settledLayerSkips; + for (int i = 0; i < 10; i++) + { + machine->advanceAndApply(1.0f / 60.0f); + } + CHECK(StateMachineInstance::sm_settledLayerSkips == skips); + CHECK(machine->layerState(0) == idleState); + + machine->getTrigger("Trigger 1")->fire(); + machine->advanceAndApply(0.1f); + CHECK(machine->layerState(0) != idleState); +} + +TEST_CASE("Binding a data context wakes settled layers", "[state machine]") +{ + auto file = ReadRiveFile("assets/data_binding_test.riv"); + auto artboard = file->artboard("artboard-2")->instance(); + auto machine = artboard->defaultStateMachine(); + + // Without a data context no view model condition can pass: the layers + // settle on that. They wait in their entry states, which key nothing and + // have no exit time, so they freeze too. + auto skips = StateMachineInstance::sm_settledLayerSkips; + auto frozen = StateMachineInstance::sm_frozenLayerAdvances; + for (int i = 0; i < 10; i++) + { + machine->advanceAndApply(1.0f / 60.0f); + } + CHECK(StateMachineInstance::sm_settledLayerSkips > skips); + CHECK(StateMachineInstance::sm_frozenLayerAdvances > frozen); + + machine->bindViewModelInstance( + file->createDefaultViewModelInstance(artboard.get())); + machine->advanceAndApply(0.0f); + CHECK(fillColor(artboard.get()) == colorARGB(255, 255, 0, 0)); +} + +TEST_CASE("Setting a view model instance without binding wakes settled layers", + "[state machine]") +{ + // setViewModelInstance can create the data context without binding + // anything, which alone lets view model conditions evaluate. A machine + // that settled first must end up where one that never ran does. + auto file = ReadRiveFile("assets/data_binding_test.riv"); + + auto settledArtboard = file->artboard("artboard-2")->instance(); + auto settled = settledArtboard->defaultStateMachine(); + for (int i = 0; i < 10; i++) + { + settled->advanceAndApply(1.0f / 60.0f); + } + settled->setViewModelInstance( + file->createDefaultViewModelInstance(settledArtboard.get())); + settled->advanceAndApply(0.0f); + + auto freshArtboard = file->artboard("artboard-2")->instance(); + auto fresh = freshArtboard->defaultStateMachine(); + fresh->setViewModelInstance( + file->createDefaultViewModelInstance(freshArtboard.get())); + fresh->advanceAndApply(0.0f); + + for (size_t i = 0; i < settled->stateMachine()->layerCount(); i++) + { + CHECK(settled->layerState(i) == fresh->layerState(i)); + } + CHECK(fillColor(settledArtboard.get()) == fillColor(freshArtboard.get())); +} + +TEST_CASE("States whose transitions wait on an exit time never freeze", + "[state machine]") +{ + // A frozen layer stops advancing its state, and with it the clock an exit + // time reads. This file has states that key nothing and wait on view model + // conditions *and* exit times: those must keep advancing while settled, + // or the exit time would never be reached once the condition passes. + auto file = ReadRiveFile("assets/bidirectional_binding_source.riv"); + auto artboard = file->artboard("avatar_child_artboard"); + REQUIRE(artboard != nullptr); + int timed = 0; + int untimed = 0; + for (size_t m = 0; m < artboard->stateMachineCount(); m++) + { + auto stateMachine = artboard->stateMachine(m); + for (size_t l = 0; l < stateMachine->layerCount(); l++) + { + auto layer = stateMachine->layer(l); + for (size_t s = 0; s < layer->stateCount(); s++) + { + auto state = layer->state(s); + bool waitsOnExitTime = false; + for (size_t t = 0; t < state->transitionCount(); t++) + { + auto transition = state->transition(t); + waitsOnExitTime = + waitsOnExitTime || (!transition->isDisabled() && + transition->enableExitTime()); + } + CHECK(state->transitionsIgnoreTime() == !waitsOnExitTime); + (waitsOnExitTime ? timed : untimed)++; + } + } + } + CHECK(timed > 0); + CHECK(untimed > 0); +}