Xxxx improve hittest performance

In certain scenarios, there is no need to perform a hit test, so we precompute the conditions and early out from calculating the hit test.
If a shape has only listeners of type PointerDown and PointerUp, and is not an opaque target, it doesn't need to check for move events or exit events, which can save a lot of computations since it will skip most frames.

Diffs=
50bc398c4 Xxxx improve hittest performance (#7584)

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index 76eddc6..9664e69 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-a01b0467e84045a47e1b9ccd9b4a84030d490e2e
+50bc398c464061bb2ab5ac51e42901053ae63f1f
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index 679ade0..f7ffea1 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -26,6 +26,7 @@
 class Shape;
 class StateMachineLayerInstance;
 class HitComponent;
+class HitShape;
 class NestedArtboard;
 class NestedEventListener;
 class NestedEventNotifier;
@@ -135,6 +136,17 @@
     const EventReport reportedEventAt(std::size_t index) const;
     bool playsAudio() override { return true; }
     BindableProperty* bindablePropertyInstance(BindableProperty* bindableProperty);
+#ifdef TESTING
+    size_t hitComponentsCount() { return m_hitComponents.size(); };
+    HitComponent* hitComponent(size_t index)
+    {
+        if (index < m_hitComponents.size())
+        {
+            return m_hitComponents[index].get();
+        }
+        return nullptr;
+    }
+#endif
 
 private:
     std::vector<EventReport> m_reportedEvents;
@@ -154,5 +166,27 @@
     InputChanged m_inputChangedCallback = nullptr;
 #endif
 };
+
+class HitComponent
+{
+public:
+    Component* component() const { return m_component; }
+    HitComponent(Component* component, StateMachineInstance* stateMachineInstance) :
+        m_component(component), m_stateMachineInstance(stateMachineInstance)
+    {}
+    virtual ~HitComponent() {}
+    virtual HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) = 0;
+#ifdef WITH_RIVE_TOOLS
+    virtual bool hitTest(Vec2D position) const = 0;
+#endif
+#ifdef TESTING
+    int earlyOutCount = 0;
+#endif
+
+protected:
+    Component* m_component;
+    StateMachineInstance* m_stateMachineInstance;
+};
+
 } // namespace rive
 #endif
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index 274ea78..0b8a112 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -421,24 +421,6 @@
     float m_holdTime = 0.0f;
 };
 
-class HitComponent
-{
-public:
-    Component* component() const { return m_component; }
-    HitComponent(Component* component, StateMachineInstance* stateMachineInstance) :
-        m_component(component), m_stateMachineInstance(stateMachineInstance)
-    {}
-    virtual ~HitComponent() {}
-    virtual HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) = 0;
-#ifdef WITH_RIVE_TOOLS
-    virtual bool hitTest(Vec2D position) const = 0;
-#endif
-
-protected:
-    Component* m_component;
-    StateMachineInstance* m_stateMachineInstance;
-};
-
 /// Representation of a Shape from the Artboard Instance and all the listeners it
 /// triggers. Allows tracking hover and performing hit detection only once on
 /// shapes that trigger multiple listeners.
@@ -447,8 +429,16 @@
 public:
     HitShape(Component* shape, StateMachineInstance* stateMachineInstance) :
         HitComponent(shape, stateMachineInstance)
-    {}
+    {
+        if (shape->as<Shape>()->isTargetOpaque())
+        {
+            canEarlyOut = false;
+        }
+    }
     bool isHovered = false;
+    bool canEarlyOut = true;
+    bool hasDownListener = false;
+    bool hasUpListener = false;
     float hitRadius = 2;
     Vec2D previousPosition;
     std::vector<const StateMachineListener*> listeners;
@@ -474,6 +464,18 @@
 
     HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
     {
+        // If the shape doesn't have any ListenerType::move / enter / exit and the event
+        // being processed is not of the type it needs to handle. There is no need to perform
+        // a hitTest (which is relatively expensive and would be happening on every
+        // pointer move) so we early out.
+        if (canEarlyOut && (hitType != ListenerType::down || !hasDownListener) &&
+            (hitType != ListenerType::up || !hasUpListener))
+        {
+#ifdef TESTING
+            earlyOutCount++;
+#endif
+            return HitResult::none;
+        }
         auto shape = m_component->as<Shape>();
         bool isOver = canHit ? hitTest(position) : false;
         bool hoverChange = isHovered != isOver;
@@ -513,6 +515,28 @@
         return isOver ? shape->isTargetOpaque() ? HitResult::hitOpaque : HitResult::hit
                       : HitResult::none;
     }
+
+    void addListener(const StateMachineListener* stateMachineListener)
+    {
+        auto listenerType = stateMachineListener->listenerType();
+        if (listenerType == ListenerType::enter || listenerType == ListenerType::exit ||
+            listenerType == ListenerType::move)
+        {
+            canEarlyOut = false;
+        }
+        else
+        {
+            if (listenerType == ListenerType::down)
+            {
+                hasDownListener = true;
+            }
+            else if (listenerType == ListenerType::up)
+            {
+                hasUpListener = true;
+            }
+        }
+        listeners.push_back(stateMachineListener);
+    }
 };
 class HitNestedArtboard : public HitComponent
 {
@@ -753,7 +777,7 @@
                     {
                         hitShape = itr->second;
                     }
-                    hitShape->listeners.push_back(listener);
+                    hitShape->addListener(listener);
                 }
                 return true;
             });
@@ -1066,4 +1090,4 @@
         return nullptr;
     }
     return bindablePropertyInstance->second;
-}
\ No newline at end of file
+}
diff --git a/test/assets/pointer_events.riv b/test/assets/pointer_events.riv
new file mode 100644
index 0000000..c65bea3
--- /dev/null
+++ b/test/assets/pointer_events.riv
Binary files differ
diff --git a/test/hittest_test.cpp b/test/hittest_test.cpp
index 87b1429..debc2d4 100644
--- a/test/hittest_test.cpp
+++ b/test/hittest_test.cpp
@@ -184,4 +184,66 @@
     // nested toggle does not change because it's below shape
     REQUIRE(secondNestedBoolTarget->value() == true);
     delete stateMachineInstance;
+}
+
+TEST_CASE("early out on listeners", "[hittest]")
+{
+    auto file = ReadRiveFile("../../test/assets/pointer_events.riv");
+
+    auto artboard = file->artboard("art-1");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("sm-1");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    stateMachineInstance->advance(0.0f);
+    artboardInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->hitComponentsCount() == 4);
+    // Hit component with only pointer down and pointer up listeners
+    auto hitComponentWithEarlyOut = stateMachineInstance->hitComponent(0);
+    // Hit component that can't early out because it has a pointer enter event
+    auto hitComponentWithNoEarlyOut = stateMachineInstance->hitComponent(1);
+    // Hit component that can't early out because it is an opaque target
+    auto hitComponentOpaque = stateMachineInstance->hitComponent(2);
+    // Hit component that can early out on all and pointer up
+    auto hitComponentOnlyPointerDown = stateMachineInstance->hitComponent(3);
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 0);
+    stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 1);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 1);
+    stateMachineInstance->pointerExit(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
+    stateMachineInstance->pointerUp(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 3);
+    stateMachineInstance->pointerMove(rive::Vec2D(105.0f, 205.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 3);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 4);
+
+    delete stateMachineInstance;
 }
\ No newline at end of file