Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at */
#include "WRHitTester.h"
#include "AsyncPanZoomController.h"
#include "APZCTreeManager.h"
#include "TreeTraversal.h" // for BreadthFirstSearch
#include "mozilla/gfx/CompositorHitTestInfo.h"
#include "mozilla/webrender/WebRenderAPI.h"
#include "nsDebug.h" // for NS_ASSERTION
#include "nsIXULRuntime.h" // for FissionAutostart
#include "mozilla/gfx/Matrix.h"
#define APZCTM_LOG(...) \
MOZ_LOG(APZCTreeManager::sLog, LogLevel::Debug, (__VA_ARGS__))
namespace mozilla {
namespace layers {
using mozilla::gfx::CompositorHitTestFlags;
using mozilla::gfx::CompositorHitTestInvisibleToHit;
static bool CheckCloseToIdentity(const gfx::Matrix4x4& aMatrix) {
// We allow a factor of 1/2048 in the multiply part of the matrix, so that if
// we multiply by a point on a screen of size 2048 we would be off by at most
// 1 pixel approximately.
const float multiplyEps = 1 / 2048.f;
// We allow 1 pixel in the translate part of the matrix.
const float translateEps = 1.f;
if (!FuzzyEqualsAdditive(aMatrix._11, 1.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._12, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._13, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._14, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._21, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._22, 1.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._23, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._24, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._31, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._32, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._33, 1.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._34, 0.f, multiplyEps) ||
!FuzzyEqualsAdditive(aMatrix._41, 0.f, translateEps) ||
!FuzzyEqualsAdditive(aMatrix._42, 0.f, translateEps) ||
!FuzzyEqualsAdditive(aMatrix._43, 0.f, translateEps) ||
!FuzzyEqualsAdditive(aMatrix._44, 1.f, multiplyEps)) {
return false;
return true;
// Checks that within the constraints of floating point math we can invert it
// reasonably enough that multiplying by the computed inverse is close to the
// identity.
static bool CheckInvertibleWithFinitePrecision(const gfx::Matrix4x4& aMatrix) {
auto inverse = aMatrix.MaybeInverse();
if (inverse.isNothing()) {
// Should we return false?
return true;
if (!CheckCloseToIdentity(aMatrix * *inverse)) {
return false;
if (!CheckCloseToIdentity(*inverse * aMatrix)) {
return false;
return true;
IAPZHitTester::HitTestResult WRHitTester::GetAPZCAtPoint(
const ScreenPoint& aHitTestPoint,
const RecursiveMutexAutoLock& aProofOfTreeLock) {
HitTestResult hit;
RefPtr<wr::WebRenderAPI> wr = mTreeManager->GetWebRenderAPI();
if (!wr) {
// If WebRender isn't running, fall back to the root APZC.
// This is mostly for the benefit of GTests which do not
// run a WebRender instance, but gracefully falling back
// here allows those tests which are not specifically
// testing the hit-test algorithm to still work.
hit.mTargetApzc = FindRootApzcForLayersId(GetRootLayersId());
hit.mHitResult = CompositorHitTestFlags::eVisibleToHitTest;
return hit;
APZCTM_LOG("Hit-testing point %s with WR\n", ToString(aHitTestPoint).c_str());
std::vector<wr::WrHitResult> results =
Maybe<wr::WrHitResult> chosenResult;
for (const wr::WrHitResult& result : results) {
ScrollableLayerGuid guid{result.mLayersId, 0, result.mScrollId};
APZCTM_LOG("Examining result with guid %s hit info 0x%x... ",
ToString(guid).c_str(), result.mHitInfo.serialize());
if (result.mHitInfo == CompositorHitTestInvisibleToHit) {
APZCTM_LOG("skipping due to invisibility.\n");
RefPtr<HitTestingTreeNode> node =
GetTargetNode(guid, &ScrollableLayerGuid::EqualsIgnoringPresShell);
if (!node) {
APZCTM_LOG("no corresponding node found, falling back to root.\n");
#ifdef DEBUG
// We can enter here during normal codepaths for cases where the
// nsDisplayCompositorHitTestInfo item emitted a scrollId of
// NULL_SCROLL_ID to the webrender display list. The semantics of that
// is to fall back to the root APZC for the layers id, so that's what
// we do here.
// If we enter this codepath and scrollId is not NULL_SCROLL_ID, then
// that's more likely to be due to a race condition between rebuilding
// the APZ tree and updating the WR scene/hit-test information, resulting
// in WR giving us a hit result for a scene that is not active in APZ.
// Such a scenario would need debugging and fixing.
// In non-Fission mode, make this assertion non-fatal because there is
// a known issue related to inactive scroll frames that can cause this
// to fire (see bug 1634763), which is fixed in Fission mode and not
// worth fixing in non-Fission mode.
if (FissionAutostart()) {
MOZ_ASSERT(result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID);
} else {
result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID,
"Inconsistency between WebRender display list and APZ scroll data");
node = FindRootNodeForLayersId(result.mLayersId);
if (!node) {
// Should never happen, but handle gracefully in release builds just
// in case.
chosenResult = Some(result);
MOZ_ASSERT(node->GetApzc()); // any node returned must have an APZC
EventRegionsOverride flags = node->GetEventRegionsOverride();
if (flags & EventRegionsOverride::ForceEmptyHitRegion) {
// This result is inside a subtree that is invisible to hit-testing.
APZCTM_LOG("skipping due to FEHR subtree.\n");
if (!CheckInvertibleWithFinitePrecision(
.ToUnknownMatrix())) {
APZCTM_LOG("skipping due to check inverse accuracy\n");
APZCTM_LOG("selecting as chosen result.\n");
chosenResult = Some(result);
hit.mTargetApzc = node->GetApzc();
if (flags & EventRegionsOverride::ForceDispatchToContent) {
chosenResult->mHitInfo += CompositorHitTestFlags::eApzAwareListeners;
if (!chosenResult) {
return hit;
hit.mLayersId = chosenResult->mLayersId;
ScrollableLayerGuid::ViewID scrollId = chosenResult->mScrollId;
gfx::CompositorHitTestInfo hitInfo = chosenResult->mHitInfo;
Maybe<uint64_t> animationId = chosenResult->mAnimationId;
SideBits sideBits = chosenResult->mSideBits;
APZCTM_LOG("Successfully matched APZC %p (hit result 0x%x)\n",
hit.mTargetApzc.get(), hitInfo.serialize());
const bool isScrollbar =
const bool isScrollbarThumb =
const ScrollDirection direction =
? ScrollDirection::eVertical
: ScrollDirection::eHorizontal;
HitTestingTreeNode* scrollbarNode = nullptr;
if (isScrollbar || isScrollbarThumb) {
scrollbarNode = BreadthFirstSearch<ReverseIterator>(
GetRootNode(), [&](HitTestingTreeNode* aNode) {
return (aNode->GetLayersId() == hit.mLayersId) &&
(aNode->IsScrollbarNode() == isScrollbar) &&
(aNode->IsScrollThumbNode() == isScrollbarThumb) &&
(aNode->GetScrollbarDirection() == direction) &&
(aNode->GetScrollTargetId() == scrollId);
hit.mHitResult = hitInfo;
if (scrollbarNode) {
RefPtr<HitTestingTreeNode> scrollbarRef = scrollbarNode;
InitializeHitTestingTreeNodeAutoLock(hit.mScrollbarNode, aProofOfTreeLock,
hit.mFixedPosSides = sideBits;
if (animationId.isSome()) {
RefPtr<HitTestingTreeNode> positionedNode = nullptr;
positionedNode = BreadthFirstSearch<ReverseIterator>(
GetRootNode(), [&](HitTestingTreeNode* aNode) {
return (aNode->GetFixedPositionAnimationId() == animationId ||
aNode->GetStickyPositionAnimationId() == animationId);
if (positionedNode) {
MOZ_ASSERT(positionedNode->GetLayersId() == chosenResult->mLayersId,
"Found node layers id does not match the hit result");
MOZ_ASSERT((positionedNode->GetFixedPositionAnimationId().isSome() ||
"A a matching fixed/sticky position node should be found");
InitializeHitTestingTreeNodeAutoLock(hit.mNode, aProofOfTreeLock,
if (hit.mNode && hit.mNode->GetFixedPositionAnimationId().isSome()) {
// If the hit element is a fixed position element, the side bits from
// the hit-result item tag are used. For now just ensure that these
// match what is found in the hit-testing tree node.
MOZ_ASSERT(sideBits == hit.mNode->GetFixedPosSides(),
"Fixed position side bits do not match");
} else if (hit.mTargetApzc && hit.mTargetApzc->IsRootContent()) {
// If the hit element is not a fixed position element, then the hit test
// result item's side bits should not be populated.
MOZ_ASSERT(sideBits == SideBits::eNone,
"Hit test results have side bits only for pos:fixed");
hit.mHitOverscrollGutter =
hit.mTargetApzc && hit.mTargetApzc->IsInOverscrollGutter(aHitTestPoint);
return hit;
} // namespace layers
} // namespace mozilla