| #ifndef _RIVE_STATE_MACHINE_INSTANCE_CLUSTERS_HPP_ | |
| #define _RIVE_STATE_MACHINE_INSTANCE_CLUSTERS_HPP_ | |
| // Implementation detail of StateMachineInstance. Only the translation units | |
| // that define StateMachineInstance members need it — state_machine_instance.cpp | |
| // and src/input/gamepad_batch.cpp — and state_machine_instance.hpp forward | |
| // declares these types rather than including this, so its heavier includes | |
| // (listener groups, SemanticManager, GamepadSnapshot) stay out of the public | |
| // surface. Do not include it just to reach a state machine; everything callers | |
| // need is on StateMachineInstance itself. | |
| // | |
| // StateMachineInstance is built once per row of an ArtboardComponentList, so | |
| // every inline byte is multiplied by the row count. The clusters below hold the | |
| // state that is untouched on a plain state machine — events, bindable-property | |
| // instancing, focus / keyboard / gamepad / semantics, and scripting — and hang | |
| // off the instance through a `Sidecar` (8 B, null until the feature is | |
| // actually used). Read paths null-check the sidecar, which is strictly cheaper | |
| // than the per-container `.empty()` probes it replaces; write paths call the | |
| // matching `ensure...()` on StateMachineInstance. | |
| #include "rive/animation/gamepad_listener_group.hpp" | |
| #include "rive/animation/keyboard_listener_group.hpp" | |
| #include "rive/animation/semantic_listener_group.hpp" | |
| #include "rive/event_report.hpp" | |
| #include "rive/input/gamepad_snapshot.hpp" | |
| #include "rive/pointer_button.hpp" | |
| #include "rive/semantic/semantic_manager.hpp" | |
| #include <cstdint> | |
| #include <memory> | |
| #include <unordered_map> | |
| #include <utility> | |
| #include <vector> | |
| namespace rive | |
| { | |
| class BindableProperty; | |
| class BindablePropertyNumber; | |
| class Core; | |
| class DataBind; | |
| class Component; | |
| class FocusListenerGroup; | |
| class HitComponent; | |
| class HitDrawable; | |
| class ListenerGroup; | |
| class ListenerViewModel; | |
| class ScriptedDrawable; | |
| class ScriptedObject; | |
| class StateMachineLayerInstance; | |
| class ViewModelInstanceValue; | |
| /// Event and viewModel-listener reporting. Allocated the first time the file | |
| /// reports an event or registers a viewModel listener. | |
| struct SMIReporting | |
| { | |
| /// Events reported since the last applyEvents, awaiting delivery. | |
| std::vector<EventReport> reportedEvents; | |
| /// The batch currently being delivered. The reported/reporting split is | |
| /// load-bearing for re-entrancy, not just capacity caching: a listener | |
| /// action can re-enter reportEvent() while this batch is being notified, | |
| /// and those new events must land in `reportedEvents` for the next pass. | |
| std::vector<EventReport> reportingEvents; | |
| /// Events first reported inside the applyEvents loop; kept host visible | |
| /// until the next applyEvents so hosts see them exactly once. | |
| std::vector<EventReport> eventsAppliedDuringLoop; | |
| /// Owned, one per viewModel listener declared by the state machine. | |
| std::vector<ListenerViewModel*> listenerViewModels; | |
| std::vector<ListenerViewModel*> reportedListenerViewModels; | |
| std::vector<ListenerViewModel*> reportingListenerViewModels; | |
| }; | |
| /// Shared BindableProperty -> an instance's clone of it. Transition conditions | |
| /// and blend states look their clone up every time they are evaluated, so this | |
| /// is a flat open-addressing table keyed by the pointer: one multiply to hash, | |
| /// and the entry sits in a contiguous array, where an unordered_map also | |
| /// divides and chases a separately allocated node for every lookup. | |
| class BindablePropertyInstances | |
| { | |
| public: | |
| BindableProperty* find(const BindableProperty* shared) const | |
| { | |
| if (m_entries.empty()) | |
| { | |
| return nullptr; | |
| } | |
| const size_t mask = m_entries.size() - 1; | |
| for (size_t i = slotOf(shared) & mask;; i = (i + 1) & mask) | |
| { | |
| const Entry& entry = m_entries[i]; | |
| if (entry.shared == shared) | |
| { | |
| return entry.instance; | |
| } | |
| if (entry.shared == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| } | |
| } | |
| /// Adds [shared] -> [instance]; [shared] must not be in the table yet. | |
| void insert(BindableProperty* shared, BindableProperty* instance) | |
| { | |
| // Keep at least half the entries empty so probes stay short. | |
| if ((m_count + 1) * 2 > m_entries.size()) | |
| { | |
| grow(); | |
| } | |
| place(shared, instance); | |
| m_count++; | |
| } | |
| template <typename F> void forEachInstance(F&& visit) const | |
| { | |
| for (const Entry& entry : m_entries) | |
| { | |
| if (entry.shared != nullptr) | |
| { | |
| visit(entry.instance); | |
| } | |
| } | |
| } | |
| void clear() | |
| { | |
| m_entries.clear(); | |
| m_count = 0; | |
| } | |
| private: | |
| struct Entry | |
| { | |
| BindableProperty* shared = nullptr; | |
| BindableProperty* instance = nullptr; | |
| }; | |
| // Fibonacci hashing: the multiply spreads the pointer's bits, so | |
| // neighbouring allocations still land far apart. | |
| static size_t slotOf(const BindableProperty* shared) | |
| { | |
| return ( | |
| size_t)(((uint64_t)(uintptr_t)shared * UINT64_C(0x9E3779B97F4A7C15)) >> | |
| 32); | |
| } | |
| void place(BindableProperty* shared, BindableProperty* instance) | |
| { | |
| const size_t mask = m_entries.size() - 1; | |
| size_t i = slotOf(shared) & mask; | |
| while (m_entries[i].shared != nullptr) | |
| { | |
| i = (i + 1) & mask; | |
| } | |
| m_entries[i] = {shared, instance}; | |
| } | |
| void grow() | |
| { | |
| std::vector<Entry> old = std::move(m_entries); | |
| m_entries.assign(old.empty() ? 8 : old.size() * 2, Entry{}); | |
| for (const Entry& entry : old) | |
| { | |
| if (entry.shared != nullptr) | |
| { | |
| place(entry.shared, entry.instance); | |
| } | |
| } | |
| } | |
| // Power-of-two size; a null shared pointer marks an empty entry. | |
| std::vector<Entry> m_entries; | |
| size_t m_count = 0; | |
| }; | |
| /// Which view model changes are pending for one layer of a state machine | |
| /// instance, see StateMachineInstance::viewModelValueChanged. Positions in | |
| /// ViewModelInstanceValue's change sequence: changes after baseline are pending | |
| /// this frame, and searched is where the layer's latest transition search | |
| /// started, which becomes its next frame's baseline. | |
| struct SMILayerChangeWindow | |
| { | |
| uint64_t baseline = 0; | |
| uint64_t searched = 0; | |
| }; | |
| /// Per-instance clones of the BindableProperty / StateTransition values a data | |
| /// bind writes to, so instances never write through to shared file data. | |
| /// Allocated only when a state machine data bind targets one of them. | |
| struct SMIBindables | |
| { | |
| /// Shared BindableProperty -> this instance's owned clone. | |
| BindablePropertyInstances propertyInstances; | |
| std::unordered_map<BindableProperty*, DataBind*> dataBindsToTarget; | |
| std::unordered_map<BindableProperty*, DataBind*> dataBindsToSource; | |
| /// Map from shared StateTransition* to per-instance BindablePropertyNumber | |
| /// instances, keyed by original property key. Data binds write to these | |
| /// instead of the shared StateTransition object. | |
| std::unordered_map< | |
| const Core*, | |
| std::unordered_map<uint32_t, BindablePropertyNumber*>> | |
| transitionPropertyInstances; | |
| /// One SMILayerChangeWindow per layer, in layer order, when the machine's | |
| /// conditions read pending changes (StateMachine::readsPendingChanges). | |
| /// Held per layer rather than per instance: a layer's transition can make | |
| /// a change after an earlier layer searched, and nothing guarantees that | |
| /// layer searches again before the frame ends (a host's settle loop stops | |
| /// once nothing is dirty, or at its cap). | |
| std::unique_ptr<SMILayerChangeWindow[]> layerChanges; | |
| /// The instance's own window: where changes pending for it start, which | |
| /// is the window a machine made or reset while this one advances starts | |
| /// in (see forgetViewModelChanges), and where its latest search started. | |
| /// With layerChanges, the baseline is the earliest layer's. | |
| uint64_t changeBaseline = 0; | |
| uint64_t changeSearched = 0; | |
| /// Where the sequence stood when this instance last handled its view | |
| /// model listeners' reports (at the start of its frame), or was created or | |
| /// reset. A trigger fired after it is pending for a listener that binds | |
| /// now, see StateMachineInstance::viewModelTriggerPendingForListeners. | |
| /// Not changeBaseline: settled layers don't search, so that one can stay | |
| /// behind fires that listeners have long handled. | |
| uint64_t listenerBaseline = 0; | |
| /// The values each layer's taken transitions read this frame. Written | |
| /// through the const instance the conditions are evaluated with. | |
| mutable std::vector<std::pair< | |
| const StateMachineLayerInstance*, | |
| const ViewModelInstanceValue*>> | |
| changesUsed; | |
| }; | |
| struct QueuedFocusEvent | |
| { | |
| FocusListenerGroup* group; | |
| bool isFocus; | |
| }; | |
| struct QueuedSemanticEvent | |
| { | |
| SemanticListenerGroup* group; | |
| SemanticActionType actionType; | |
| }; | |
| /// A capture-phase pointer listener on one of the hit drawables its target | |
| /// resolves to: a group's listener flattens to its leaf shapes, one each. | |
| struct CaptureHit | |
| { | |
| HitDrawable* hit; | |
| ListenerGroup* group; | |
| /// The listener's target (a layout, not its proxy), which is what a claim | |
| /// covers. | |
| Component* target; | |
| /// How deep target sits under the artboard. Capture runs shallow first. | |
| uint32_t depth; | |
| /// hit's place in the hit components, topmost first. Breaks depth ties, | |
| /// and tells what is drawn above it. | |
| size_t drawIndex; | |
| }; | |
| /// A press a capture listener claimed, held until that pointer's release. | |
| struct PointerClaim | |
| { | |
| int pointerId; | |
| PointerButton button; | |
| /// The claiming listener's target. | |
| Component* scope; | |
| /// What the claim turned off: hit components under scope (whole nested | |
| /// machines included), and capture listeners on targets under it. | |
| std::vector<HitComponent*> hits; | |
| std::vector<ListenerGroup*> captureGroups; | |
| }; | |
| /// Everything driven by focus, keyboard, gamepad, or accessibility rather than | |
| /// by pointer events. Allocated when the file declares a focus/blur, keyboard, | |
| /// textInput, gamepad, or semanticAction listener, when a script wants keyboard | |
| /// or gamepad input, or when enableSemantics() is called. | |
| struct SMIInputExtras | |
| { | |
| std::vector<std::unique_ptr<FocusListenerGroup>> focusListenerGroups; | |
| std::vector<std::unique_ptr<KeyboardListenerGroup>> keyboardListenerGroups; | |
| std::vector<std::unique_ptr<GamepadListenerGroup>> gamepadListenerGroups; | |
| std::vector<std::unique_ptr<SemanticListenerGroup>> semanticListenerGroups; | |
| /// Non-owning back-references to every `ScriptedDrawable` whose script | |
| /// declares a gamepad handler. Populated once at init from the artboard | |
| /// (which outlives this state machine) and walked by | |
| /// `broadcastGamepadToScriptedDrawables` so events reach scripts that are | |
| /// not on the focus chain. Mirrors how `m_hitComponents` lets every | |
| /// pointer-aware drawable react regardless of focus. | |
| std::vector<ScriptedDrawable*> gamepadScriptedDrawables; | |
| /// Latest embedder gamepad state for `submitGamepadsFromBuffer` (WASM/JS). | |
| std::unordered_map<int, GamepadSnapshot> embedderGamepads; | |
| std::unique_ptr<SemanticManager> semanticManager; | |
| SemanticManager* externalSemanticManager = nullptr; | |
| /// Queued for deferred execution during advance(). | |
| std::vector<QueuedFocusEvent> queuedFocusEvents; | |
| std::vector<QueuedSemanticEvent> queuedSemanticEvents; | |
| /// Capture-phase pointer listeners. Their groups are kept apart from the | |
| /// instance's listener groups, which the normal phase resets on every | |
| /// event. Allocated with the rest of this cluster when the machine has | |
| /// one, so a machine without stays on the null-sidecar path. | |
| std::vector<std::unique_ptr<ListenerGroup>> captureGroups; | |
| /// In capture order: shallowest target first, then topmost first. | |
| std::vector<CaptureHit> captureHits; | |
| std::vector<PointerClaim> pointerClaims; | |
| /// treeHasCaptureListeners' answer, and the capture topology epoch it | |
| /// was computed at (0: never). | |
| uint32_t captureTreeEpoch = 0; | |
| bool captureTreeHas = false; | |
| }; | |
| /// Per-instance clones of the state machine's scripted objects. Allocated only | |
| /// when the file uses scripting. | |
| struct SMIScripting | |
| { | |
| /// (shared source, owned instance) pairs in the state machine's authored | |
| /// order. A vector rather than a map so Lua `init` runs in a deterministic | |
| /// order; lookups are a handful of entries and are always followed by a VM | |
| /// call that dwarfs the scan. | |
| std::vector<std::pair<const ScriptedObject*, ScriptedObject*>> objects; | |
| ScriptedObject* find(const ScriptedObject* source) const | |
| { | |
| for (const auto& pair : objects) | |
| { | |
| if (pair.first == source) | |
| { | |
| return pair.second; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| }; | |
| } // namespace rive | |
| #endif |