Skip to content

Commit

Permalink
Xxxx improve hittest performance
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
bodymovin and bodymovin committed Jul 23, 2024
1 parent 8e0d982 commit 560a993
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a01b0467e84045a47e1b9ccd9b4a84030d490e2e
50bc398c464061bb2ab5ac51e42901053ae63f1f
34 changes: 34 additions & 0 deletions include/rive/animation/state_machine_instance.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class SMITrigger;
class Shape;
class StateMachineLayerInstance;
class HitComponent;
class HitShape;
class NestedArtboard;
class NestedEventListener;
class NestedEventNotifier;
Expand Down Expand Up @@ -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<EventReport> m_reportedEvents;
Expand All @@ -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
66 changes: 45 additions & 21 deletions src/animation/state_machine_instance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -447,8 +429,16 @@ class HitShape : public HitComponent
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;
Expand All @@ -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<Shape>();
bool isOver = canHit ? hitTest(position) : false;
bool hoverChange = isHovered != isOver;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -753,7 +777,7 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine,
{
hitShape = itr->second;
}
hitShape->listeners.push_back(listener);
hitShape->addListener(listener);
}
return true;
});
Expand Down Expand Up @@ -1066,4 +1090,4 @@ BindableProperty* StateMachineInstance::bindablePropertyInstance(BindablePropert
return nullptr;
}
return bindablePropertyInstance->second;
}
}
Binary file added test/assets/pointer_events.riv
Binary file not shown.
62 changes: 62 additions & 0 deletions test/hittest_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit 560a993

Please sign in to comment.