/* -*- 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 http://mozilla.org/MPL/2.0/. */ #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 = 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 results = wr->HitTest(wr::ToWorldPoint(aHitTestPoint)); Maybe 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"); continue; } RefPtr 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 { NS_ASSERTION( result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID, "Inconsistency between WebRender display list and APZ scroll data"); } #endif node = FindRootNodeForLayersId(result.mLayersId); if (!node) { // Should never happen, but handle gracefully in release builds just // in case. MOZ_ASSERT(false); chosenResult = Some(result); break; } } 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"); continue; } if (!CheckInvertibleWithFinitePrecision( mTreeManager->GetScreenToApzcTransform(node->GetApzc()) .ToUnknownMatrix())) { APZCTM_LOG("skipping due to check inverse accuracy\n"); continue; } APZCTM_LOG("selecting as chosen result.\n"); chosenResult = Some(result); hit.mTargetApzc = node->GetApzc(); if (flags & EventRegionsOverride::ForceDispatchToContent) { chosenResult->mHitInfo += CompositorHitTestFlags::eApzAwareListeners; } break; } if (!chosenResult) { return hit; } MOZ_ASSERT(hit.mTargetApzc); hit.mLayersId = chosenResult->mLayersId; ScrollableLayerGuid::ViewID scrollId = chosenResult->mScrollId; gfx::CompositorHitTestInfo hitInfo = chosenResult->mHitInfo; Maybe 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 = hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbar); const bool isScrollbarThumb = hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarThumb); const ScrollDirection direction = hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarVertical) ? ScrollDirection::eVertical : ScrollDirection::eHorizontal; HitTestingTreeNode* scrollbarNode = nullptr; if (isScrollbar || isScrollbarThumb) { scrollbarNode = BreadthFirstSearch( 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 scrollbarRef = scrollbarNode; InitializeHitTestingTreeNodeAutoLock(hit.mScrollbarNode, aProofOfTreeLock, scrollbarRef); } hit.mFixedPosSides = sideBits; if (animationId.isSome()) { RefPtr positionedNode = nullptr; positionedNode = BreadthFirstSearch( 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() || positionedNode->GetStickyPositionAnimationId().isSome()), "A a matching fixed/sticky position node should be found"); InitializeHitTestingTreeNodeAutoLock(hit.mNode, aProofOfTreeLock, positionedNode); } #if defined(MOZ_WIDGET_ANDROID) 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"); } #endif } hit.mHitOverscrollGutter = hit.mTargetApzc && hit.mTargetApzc->IsInOverscrollGutter(aHitTestPoint); return hit; } } // namespace layers } // namespace mozilla