| #include "rive/animation/listener_invocation.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| #include "rive/core/binary_reader.hpp" | |
| #include "rive/input/focus_manager.hpp" | |
| #include "rive/input/gamepad_batch.hpp" | |
| #include "rive/input/gamepad_snapshot.hpp" | |
| #include "rive/input/standard_gamepad.hpp" | |
| #include "rive/scripted/scripted_drawable.hpp" | |
| #include "rive/span.hpp" | |
| // Defines StateMachineInstance's SMIInputExtras cluster, which owns the | |
| // embedder gamepad snapshots and the gamepad-aware scripted drawables. | |
| #include "rive/animation/state_machine_instance_clusters.hpp" | |
| namespace rive | |
| { | |
| static void recomputeButtonMaskFromValues(GamepadSnapshot& s) | |
| { | |
| s.buttonMask = 0; | |
| for (size_t i = 0; i < s.buttonValues.size(); i++) | |
| { | |
| if (s.buttonValues[i] >= 0.5f) | |
| { | |
| s.buttonMask |= (uint64_t(1) << i); | |
| } | |
| } | |
| } | |
| static void fillStandardIntent(const GamepadSnapshot& snap, | |
| GamepadEventInvocation& ev) | |
| { | |
| ev.hasStandardButtonIntent = false; | |
| ev.hasStandardAxisIntent = false; | |
| if (snap.mapping != GamepadMappingKind::standard) | |
| { | |
| return; | |
| } | |
| if (ev.change.kind == GamepadInputChangeKind::button) | |
| { | |
| if (ev.change.index <= | |
| static_cast<uint8_t>(StandardGamepadButton::start)) | |
| { | |
| ev.hasStandardButtonIntent = true; | |
| ev.standardButton = | |
| static_cast<StandardGamepadButton>(ev.change.index); | |
| } | |
| } | |
| else | |
| { | |
| if (ev.change.index <= | |
| static_cast<uint8_t>(StandardGamepadAxis::rightTrigger)) | |
| { | |
| ev.hasStandardAxisIntent = true; | |
| ev.standardAxis = static_cast<StandardGamepadAxis>(ev.change.index); | |
| } | |
| } | |
| } | |
| static bool readConnectedPayload(BinaryReader& reader, | |
| GamepadSnapshot& outSnapshot) | |
| { | |
| outSnapshot = GamepadSnapshot{}; | |
| outSnapshot.deviceId = static_cast<int32_t>(reader.readUint32()); | |
| const uint8_t mappingByte = reader.readByte(); | |
| const uint8_t nButtons = reader.readByte(); | |
| const uint8_t nAxes = reader.readByte(); | |
| reader.readByte(); // 1-byte padding to align the float arrays. | |
| if (reader.hasError() || nButtons > kGamepadBatchMaxButtons || | |
| nAxes > kGamepadBatchMaxAxes) | |
| { | |
| return false; | |
| } | |
| outSnapshot.mapping = mappingByte == 0 ? GamepadMappingKind::standard | |
| : GamepadMappingKind::unknown; | |
| outSnapshot.buttonValues.resize(nButtons); | |
| outSnapshot.axes.resize(nAxes); | |
| for (uint8_t b = 0; b < nButtons; b++) | |
| { | |
| outSnapshot.buttonValues[b] = reader.readFloat32(); | |
| } | |
| for (uint8_t a = 0; a < nAxes; a++) | |
| { | |
| outSnapshot.axes[a] = reader.readFloat32(); | |
| } | |
| if (reader.hasError()) | |
| { | |
| return false; | |
| } | |
| // buttonMask is not on the wire — derive it from the per-button values. | |
| recomputeButtonMaskFromValues(outSnapshot); | |
| return true; | |
| } | |
| static bool applyUpdateChange(GamepadSnapshot& snap, | |
| const GamepadInputChange& ch) | |
| { | |
| if (ch.kind == GamepadInputChangeKind::button) | |
| { | |
| if (ch.index >= kGamepadBatchMaxButtons) | |
| { | |
| return false; | |
| } | |
| if (snap.buttonValues.size() <= ch.index) | |
| { | |
| snap.buttonValues.resize(ch.index + 1, 0.f); | |
| } | |
| snap.buttonValues[ch.index] = ch.value; | |
| recomputeButtonMaskFromValues(snap); | |
| } | |
| else | |
| { | |
| if (ch.index >= kGamepadBatchMaxAxes) | |
| { | |
| return false; | |
| } | |
| if (snap.axes.size() <= ch.index) | |
| { | |
| snap.axes.resize(ch.index + 1, 0.f); | |
| } | |
| snap.axes[ch.index] = ch.value; | |
| } | |
| return true; | |
| } | |
| static GamepadEventInvocation makeEventInvocation( | |
| const GamepadSnapshot& afterApply, | |
| const GamepadInputChange& ch) | |
| { | |
| GamepadEventInvocation ev; | |
| ev.fullState = afterApply; | |
| ev.change = ch; | |
| fillStandardIntent(afterApply, ev); | |
| return ev; | |
| } | |
| // Decode a little-endian binary batch of gamepad events produced by the | |
| // embedder (e.g. the JS runtime — see registerGamepadInteractions.ts) and | |
| // dispatch them through the focus manager. | |
| // | |
| // Wire format (all multi-byte values little-endian, fixed-width — no LEB128): | |
| // uint32 version -- must equal kGamepadBatchWireVersion | |
| // then a stream of records, each starting with a 1-byte GamepadRecordType: | |
| // | |
| // connected (0): | |
| // int32 deviceId | |
| // uint8 mapping (0 = standard, else unknown) | |
| // uint8 nButtons (<= kGamepadBatchMaxButtons) | |
| // uint8 nAxes (<= kGamepadBatchMaxAxes) | |
| // uint8 padding (alignment filler) | |
| // float32 buttonValues[nButtons] | |
| // float32 axes[nAxes] | |
| // | |
| // update (1): | |
| // int32 deviceId | |
| // uint8 nChanges | |
| // repeated nChanges times: | |
| // uint8 kind (0 = button, 1 = axis) | |
| // uint8 index | |
| // float32 value | |
| // | |
| // disconnected (2): | |
| // int32 deviceId | |
| // | |
| // Returns false (and stops dispatching) on any malformed record: short read, | |
| // wrong version, unknown device on update, out-of-range button/axis index, | |
| // or unknown record type. A truncated batch leaves any records dispatched so | |
| // far in place. | |
| bool StateMachineInstance::submitGamepadsFromBuffer(const uint8_t* data, | |
| size_t n) | |
| { | |
| if (data == nullptr) | |
| { | |
| return false; | |
| } | |
| BinaryReader reader(Span<const uint8_t>(data, n)); | |
| const uint32_t version = reader.readUint32(); | |
| if (reader.hasError() || version != kGamepadBatchWireVersion) | |
| { | |
| return false; | |
| } | |
| FocusManager* fm = focusManager(); | |
| if (fm == nullptr) | |
| { | |
| return false; | |
| } | |
| // An embedder submitting a batch is exactly the moment gamepad state | |
| // starts existing on this instance, so allocate the cluster up front and | |
| // keep the pointer for the whole drain. | |
| SMIInputExtras& extras = ensureInputExtras(); | |
| // reachedEnd() returns true once the cursor hits end-of-buffer OR the | |
| // reader has flagged an overflow, so any partial-record read inside the | |
| // loop will both `return false` and naturally terminate iteration. | |
| while (!reader.reachedEnd()) | |
| { | |
| const uint8_t typeByte = reader.readByte(); | |
| if (reader.hasError()) | |
| { | |
| return false; | |
| } | |
| const auto rec = static_cast<GamepadRecordType>(typeByte); | |
| switch (rec) | |
| { | |
| case GamepadRecordType::connected: | |
| { | |
| GamepadSnapshot snap; | |
| if (!readConnectedPayload(reader, snap)) | |
| { | |
| return false; | |
| } | |
| extras.embedderGamepads[snap.deviceId] = snap; | |
| { | |
| auto invocation = | |
| ListenerInvocation::gamepadConnected(snap); | |
| ScriptedDrawable* dispatched = nullptr; | |
| (void)fm->gamepadDispatch(invocation, &dispatched); | |
| broadcastGamepadToScriptedDrawables(invocation, dispatched); | |
| } | |
| break; | |
| } | |
| case GamepadRecordType::update: | |
| { | |
| const int32_t deviceId = | |
| static_cast<int32_t>(reader.readUint32()); | |
| const uint8_t nChanges = reader.readByte(); | |
| if (reader.hasError()) | |
| { | |
| return false; | |
| } | |
| // Updates must target a gamepad we've previously seen | |
| // connected; otherwise we have no snapshot to mutate. | |
| auto it = extras.embedderGamepads.find(deviceId); | |
| if (it == extras.embedderGamepads.end()) | |
| { | |
| return false; | |
| } | |
| std::vector<GamepadInputChange> changes; | |
| changes.reserve(nChanges); | |
| for (uint8_t i = 0; i < nChanges; i++) | |
| { | |
| const uint8_t chKindB = reader.readByte(); | |
| const uint8_t chIndex = reader.readByte(); | |
| const float fval = reader.readFloat32(); | |
| if (reader.hasError()) | |
| { | |
| return false; | |
| } | |
| GamepadInputChange ch; | |
| ch.kind = chKindB == 0 ? GamepadInputChangeKind::button | |
| : GamepadInputChangeKind::axis; | |
| ch.index = chIndex; | |
| ch.value = fval; | |
| changes.push_back(ch); | |
| } | |
| // Apply all changes to the stored snapshot first so that every | |
| // dispatched event sees the same fully-updated `fullState`... | |
| GamepadSnapshot& snap = it->second; | |
| for (const auto& ch : changes) | |
| { | |
| if (!applyUpdateChange(snap, ch)) | |
| { | |
| return false; | |
| } | |
| } | |
| // ...then dispatch one event per change carrying that final | |
| // state plus the specific change that occurred. | |
| const GamepadSnapshot finalSnap = snap; | |
| for (const auto& ch : changes) | |
| { | |
| GamepadEventInvocation evi = | |
| makeEventInvocation(finalSnap, ch); | |
| auto invocation = | |
| ListenerInvocation::gamepadEvent(std::move(evi)); | |
| ScriptedDrawable* dispatched = nullptr; | |
| (void)fm->gamepadDispatch(invocation, &dispatched); | |
| broadcastGamepadToScriptedDrawables(invocation, dispatched); | |
| } | |
| break; | |
| } | |
| case GamepadRecordType::disconnected: | |
| { | |
| const int32_t deviceId = | |
| static_cast<int32_t>(reader.readUint32()); | |
| if (reader.hasError()) | |
| { | |
| return false; | |
| } | |
| extras.embedderGamepads.erase(deviceId); | |
| { | |
| auto invocation = | |
| ListenerInvocation::gamepadDisconnected(deviceId); | |
| ScriptedDrawable* dispatched = nullptr; | |
| (void)fm->gamepadDispatch(invocation, &dispatched); | |
| broadcastGamepadToScriptedDrawables(invocation, dispatched); | |
| } | |
| break; | |
| } | |
| default: | |
| // Unknown record type — wire format mismatch, bail out. | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| HitResult StateMachineInstance::broadcastGamepadToScriptedDrawables( | |
| const ListenerInvocation& invocation, | |
| ScriptedDrawable* alreadyDispatched) | |
| { | |
| bool hitSomething = false; | |
| bool hitOpaque = false; | |
| // Walk m_hitComponents purely for the nested-propagation overrides | |
| // (HitNestedArtboard / HitArtboardComponentList recurse into their | |
| // child SMIs). HitScriptedDrawable's gamepad path is intentionally a | |
| // no-op — direct dispatch happens via gamepadScriptedDrawables below | |
| // so scripts without pointer handlers are still reached. | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| HitResult hitResult = | |
| hitShape->processGamepadInvocation(invocation, alreadyDispatched); | |
| if (hitResult != HitResult::none) | |
| { | |
| hitSomething = true; | |
| if (hitResult == HitResult::hitOpaque) | |
| { | |
| hitOpaque = true; | |
| } | |
| } | |
| } | |
| #ifdef WITH_RIVE_SCRIPTING | |
| // Null whenever no script in this artboard declared a gamepad handler, | |
| // which is the common case — nothing to broadcast to. | |
| if (auto* extras = inputExtras()) | |
| { | |
| for (ScriptedDrawable* drawable : extras->gamepadScriptedDrawables) | |
| { | |
| if (drawable == nullptr || drawable == alreadyDispatched) | |
| { | |
| continue; | |
| } | |
| switch (invocation.kind()) | |
| { | |
| case ListenerInvocationKind::gamepadConnected: | |
| if (!drawable->wantsGamePadConnect()) | |
| { | |
| continue; | |
| } | |
| break; | |
| case ListenerInvocationKind::gamepadEvent: | |
| if (!drawable->wantsGamePadEvent()) | |
| { | |
| continue; | |
| } | |
| break; | |
| case ListenerInvocationKind::gamepadDisconnected: | |
| if (!drawable->wantsGamePadDisconnect()) | |
| { | |
| continue; | |
| } | |
| break; | |
| default: | |
| continue; | |
| } | |
| if (drawable->gamepadDispatch(invocation)) | |
| { | |
| hitSomething = true; | |
| } | |
| } | |
| } | |
| #endif | |
| return hitSomething ? hitOpaque ? HitResult::hitOpaque : HitResult::hit | |
| : HitResult::none; | |
| } | |
| } // namespace rive |