From 560a993838180d05490ebe93c9e5b8c3a134c369 Mon Sep 17 00:00:00 2001 From: bodymovin Date: Tue, 23 Jul 2024 20:53:06 +0000 Subject: [PATCH] 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 --- .rive_head | 2 +- .../rive/animation/state_machine_instance.hpp | 34 +++++++++ src/animation/state_machine_instance.cpp | 66 ++++++++++++------ test/assets/pointer_events.riv | Bin 0 -> 535 bytes test/hittest_test.cpp | 62 ++++++++++++++++ 5 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 test/assets/pointer_events.riv diff --git a/.rive_head b/.rive_head index 76eddc60..9664e694 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 679ade06..f7ffea16 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp @@ -26,6 +26,7 @@ class SMITrigger; class Shape; class StateMachineLayerInstance; class HitComponent; +class HitShape; class NestedArtboard; class NestedEventListener; class NestedEventNotifier; @@ -135,6 +136,17 @@ class StateMachineInstance : public Scene, public NestedEventNotifier, public Ne 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 m_reportedEvents; @@ -154,5 +166,27 @@ class StateMachineInstance : public Scene, public NestedEventNotifier, public Ne 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 274ea78f..0b8a112d 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp @@ -421,24 +421,6 @@ class StateMachineLayerInstance 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 @@ class HitShape : public HitComponent public: HitShape(Component* shape, StateMachineInstance* stateMachineInstance) : HitComponent(shape, stateMachineInstance) - {} + { + if (shape->as()->isTargetOpaque()) + { + canEarlyOut = false; + } + } bool isHovered = false; + bool canEarlyOut = true; + bool hasDownListener = false; + bool hasUpListener = false; float hitRadius = 2; Vec2D previousPosition; std::vector listeners; @@ -474,6 +464,18 @@ class HitShape : public HitComponent 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(); bool isOver = canHit ? hitTest(position) : false; bool hoverChange = isHovered != isOver; @@ -513,6 +515,28 @@ class HitShape : public HitComponent 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 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine, { hitShape = itr->second; } - hitShape->listeners.push_back(listener); + hitShape->addListener(listener); } return true; }); @@ -1066,4 +1090,4 @@ BindableProperty* StateMachineInstance::bindablePropertyInstance(BindablePropert 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 0000000000000000000000000000000000000000..c65bea3c16a06e93ae80e67154e64f365b22d7a8 GIT binary patch literal 535 zcmZvX%}T>i5QS&V#FT(mYpvo&H!ictLW+pL)rH_n#C_0<1k$A5CQ5g@&0c*0AH;|7 z0aE&-{h?r&Zgi3;q~e8{yZ8=s&N)3kKhoeO#1HCHpGL}}<3zu=j}bO^h|t+){O2>XNs;$qwx$AnN+;~^f6N9O@p zE$e4?RXDaQ)=bzaQ@zT?wnCh?>xbU03!5QoCssp9#=EfpgbM7y9&A4#fDEx9#f5Kp zHS0PMhHuF$;YX-7aSDPKLYsdU{&{KxqP$c*{(07k*f4;xNo%Of<^!U literal 0 HcmV?d00001 diff --git a/test/hittest_test.cpp b/test/hittest_test.cpp index 87b14296..debc2d47 100644 --- a/test/hittest_test.cpp +++ b/test/hittest_test.cpp @@ -183,5 +183,67 @@ TEST_CASE("hit test on opaque nested artboard", "[hittest]") // 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