0

Fall back to main thread if scroll hit test is affected by rounded corners

We had two issues:
1.  Before we had fast rounded corners, we always created mask layers
for rounded corner clips, and the mask layer made the scroll begin
unreliable and fall back to the main thread. With fast rounded corners,
the scrolls were treated as reliable without checking if the point is
in or out of the rounded corners.
2. If the scroller has a rounded corner by itself (instead of from an
ancestor), as we only create InnerBorderRadiusClip for the contents,
the compositor doesn't actually know which part of the layer bounds
is transparent to hit test (e.g. if the scroller has a border which
is outside of the InnerBorderRadiusClip). Now with HitTestOpaqueness,
such layers have HitTestOpaqueness::kMixed.

This CL changes the behavior of
LayerTreeImpl::FindLayersUpToFirstOpaqueToHitTest (renamed from
FindLayerUpToFirstScrollableOrOpaqueToHitTest):
- For issue : LayerImpl::OpaqueToHitTest() also checks whether the
  layer is affected by any fast rounded corners;
- For issue : FindLayerUpToFirstOpaqueToHitTest checks only
  OpaqueToHitTest() (without checking IsScrollerOrScrollbar())
  because a hit test on a scrollable layer is reliable only if it's
  opaque to hit test.

Bug: 40277896
Change-Id: I1acb16f2c6790760661e8239ea1599035f83ea51
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5466909
Commit-Queue: Xianzhu Wang <wangxianzhu@chromium.org>
Reviewed-by: Steve Kobes <skobes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1291538}
This commit is contained in:
Xianzhu Wang
2024-04-23 21:18:12 +00:00
committed by Chromium LUCI CQ
parent 879a9882b2
commit 5141f8f29c
12 changed files with 178 additions and 14 deletions

@ -1276,11 +1276,16 @@ InputHandler::ScrollHitTestResult InputHandler::HitTestScrollNode(
ActiveTree().FindLayersUpToFirstScrollableOrOpaqueToHitTest(
device_viewport_point);
const LayerImpl* first_scrollable_or_opaque_to_hit_test_layer =
!layers.empty() && (layers.back()->IsScrollerOrScrollbar() ||
layers.back()->OpaqueToHitTest())
? layers.back()
: nullptr;
const LayerImpl* first_scrollable_or_opaque_to_hit_test_layer = nullptr;
if (!layers.empty()) {
if (compositor_delegate_->GetSettings().enable_hit_test_opaqueness) {
if (layers.back()->OpaqueToHitTest()) {
first_scrollable_or_opaque_to_hit_test_layer = layers.back();
}
} else if (layers.back()->IsScrollerOrScrollbar()) {
first_scrollable_or_opaque_to_hit_test_layer = layers.back();
}
}
ScrollNode* node_to_scroll = nullptr;
// Go through each layer up to (and including) the scroller. Any may block

@ -527,6 +527,7 @@ bool LayerImpl::IsScrollbarLayer() const {
}
bool LayerImpl::IsScrollerOrScrollbar() const {
DCHECK(!layer_tree_impl()->settings().enable_hit_test_opaqueness);
return IsScrollbarLayer() ||
GetScrollTree().FindNodeFromElementId(element_id());
}
@ -564,7 +565,10 @@ bool LayerImpl::HitTestable() const {
}
bool LayerImpl::OpaqueToHitTest() const {
return HitTestable() && hit_test_opaqueness_ == HitTestOpaqueness::kOpaque;
return HitTestable() && hit_test_opaqueness_ == HitTestOpaqueness::kOpaque &&
!GetEffectTree()
.Node(effect_tree_index())
->node_or_ancestor_has_fast_rounded_corner;
}
void LayerImpl::SetBackgroundColor(SkColor4f background_color) {

@ -143,6 +143,7 @@ struct CC_EXPORT EffectNode {
// If set, the effect node tries to not trigger a render surface due to it
// having a rounded corner.
bool is_fast_rounded_corner : 1 = false;
bool node_or_ancestor_has_fast_rounded_corner : 1 = false;
// If the node or it's parent has the filters, it sets to true.
bool node_or_ancestor_has_filters : 1 = false;
// All node in the subtree starting from the containing render surface, and

@ -166,7 +166,7 @@ class LayerTreeHostImplTest : public testing::Test,
media::InitializeMediaLibrary();
}
LayerTreeSettings DefaultSettings() {
virtual LayerTreeSettings DefaultSettings() {
LayerListSettings settings;
settings.minimum_occlusion_tracking_size = gfx::Size();
settings.enable_smooth_scroll = true;
@ -17923,6 +17923,12 @@ class UnifiedScrollingTest : public LayerTreeHostImplTest {
public:
using ScrollStatus = InputHandler::ScrollStatus;
LayerTreeSettings DefaultSettings() override {
auto settings = LayerTreeHostImplTest::DefaultSettings();
settings.enable_hit_test_opaqueness = true;
return settings;
}
void SetUp() override {
LayerTreeHostImplTest::SetUp();
@ -17949,7 +17955,9 @@ class UnifiedScrollingTest : public LayerTreeHostImplTest {
DrawFrame();
}
void CreateScroller(uint32_t main_thread_scrolling_reasons) {
void CreateScroller(
uint32_t main_thread_scrolling_reasons,
HitTestOpaqueness hit_test_opaqueness = HitTestOpaqueness::kOpaque) {
// Creates a regular compositeds scroller that comes with a ScrollNode and
// Layer.
gfx::Size scrollable_content_bounds(100, 100);
@ -17958,6 +17966,7 @@ class UnifiedScrollingTest : public LayerTreeHostImplTest {
LayerImpl* layer =
AddScrollableLayer(OuterViewportScrollLayer(), container_bounds,
scrollable_content_bounds);
layer->SetHitTestOpaqueness(hit_test_opaqueness);
scroller_layer_ = layer;
GetScrollNode(layer)->main_thread_scrolling_reasons =
main_thread_scrolling_reasons;
@ -18366,6 +18375,18 @@ TEST_F(UnifiedScrollingTest, MainThreadScrollingReasonsScrollOnCompositor) {
}
}
TEST_F(UnifiedScrollingTest, UnreliableHitTestOnNonOpaqueToHitTestScroller) {
CreateScroller(MainThreadScrollingReason::kNotScrollingOnMain,
HitTestOpaqueness::kMixed);
{
ScrollStatus status = ScrollBegin(gfx::Vector2d(0, 10));
EXPECT_EQ(ScrollThread::kScrollOnImplThread, status.thread);
EXPECT_EQ(MainThreadScrollingReason::kFailedHitTest,
status.main_thread_hit_test_reasons);
}
}
// This tests whether or not various kinds of scrolling mutates the transform
// tree or not. It is parameterized and used by tests below.
void UnifiedScrollingTest::TestUncompositedScrollingState(

@ -2577,7 +2577,9 @@ LayerTreeImpl::FindLayersUpToFirstScrollableOrOpaqueToHitTest(
std::pair<const LayerImpl*, float>(layer, distance_to_intersection));
} else {
layers.push_back(layer);
if (layer->IsScrollerOrScrollbar() || layer->OpaqueToHitTest()) {
if (settings().enable_hit_test_opaqueness
? layer->OpaqueToHitTest()
: layer->IsScrollerOrScrollbar()) {
break;
}
}
@ -2606,7 +2608,9 @@ LayerTreeImpl::FindLayersUpToFirstScrollableOrOpaqueToHitTest(
const LayerImpl* layer = pair.first;
result.push_back(layer);
if (layer->IsScrollerOrScrollbar() || layer->OpaqueToHitTest()) {
if (settings().enable_hit_test_opaqueness
? layer->OpaqueToHitTest()
: layer->IsScrollerOrScrollbar()) {
return result;
}
}

@ -626,10 +626,11 @@ class CC_EXPORT LayerTreeImpl {
LayerImpl* FindLayerThatIsHitByPointInWheelEventHandlerRegion(
const gfx::PointF& screen_space_point);
// Returns all layers up to the first scroller, scrollbar layer or a layer
// opaque to hit test, inclusive. The returned vector is sorted in order of
// top most come first. The back of the vector will be the scrollable layer
// or the first layer opaque to hit test, if one was hit.
// Returns all layers up to the first scroller or scrollbar layer (when
// enable_hit_test_opaqueness is false) or a layer opaque to hit test (when
// enable_hit_test_opaqueness is true), inclusive. The returned vector is
// sorted in order of top most come first. The back of the vector will be the
// scrollable layer or the first layer opaque to hit test, if one was hit.
std::vector<const LayerImpl*> FindLayersUpToFirstScrollableOrOpaqueToHitTest(
const gfx::PointF& screen_space_point);
bool PointHitsNonFastScrollableRegion(const gfx::PointF& scree_space_point,

@ -227,6 +227,10 @@ class CC_EXPORT LayerTreeSettings {
// This is an arbitrary limit here similar to what hardware might have.
int max_render_buffer_bounds_for_sw = 16 * 1024;
// Whether the client supports HitTestOpaqueness::kOpaque. If yes, cc will
// respect the flag and optimize scroll hit testing.
bool enable_hit_test_opaqueness = false;
// Whether to use variable refresh rates when generating begin frames.
bool enable_variable_refresh_rate = false;
};

@ -963,6 +963,15 @@ void EffectTree::UpdateHasFilters(EffectNode* node, EffectNode* parent_node) {
}
}
void EffectTree::UpdateHasFastRoundedCorner(EffectNode* node,
EffectNode* parent_node) {
node->node_or_ancestor_has_fast_rounded_corner = node->is_fast_rounded_corner;
if (parent_node) {
node->node_or_ancestor_has_fast_rounded_corner |=
parent_node->node_or_ancestor_has_fast_rounded_corner;
}
}
void EffectTree::UpdateBackfaceVisibility(EffectNode* node,
EffectNode* parent_node) {
if (parent_node && parent_node->hidden_by_backface_visibility) {
@ -1094,6 +1103,7 @@ void EffectTree::UpdateEffects(int id) {
UpdateIsDrawn(node, parent_node);
UpdateEffectChanged(node, parent_node);
UpdateHasFilters(node, parent_node);
UpdateHasFastRoundedCorner(node, parent_node);
UpdateBackfaceVisibility(node, parent_node);
UpdateHasMaskingChild(node, parent_node);
UpdateOnlyDrawsVisibleContent(node, parent_node);

@ -399,6 +399,7 @@ class CC_EXPORT EffectTree final : public PropertyTree<EffectNode> {
void UpdateEffectChanged(EffectNode* node, EffectNode* parent_node);
void UpdateHasFilters(EffectNode* node, EffectNode* parent_node);
void UpdateHasFastRoundedCorner(EffectNode* node, EffectNode* parent_node);
typedef std::unordered_multimap<int, std::unique_ptr<viz::CopyOutputRequest>>
CopyRequestMap;

@ -599,6 +599,9 @@ cc::LayerTreeSettings GenerateLayerTreeSettings(
settings.disable_frame_rate_limit =
cmd.HasSwitch(::switches::kDisableFrameRateLimit);
settings.enable_hit_test_opaqueness =
RuntimeEnabledFeatures::HitTestOpaquenessEnabled();
settings.enable_variable_refresh_rate =
::features::IsVariableRefreshRateAlwaysOn();

@ -0,0 +1,59 @@
<!doctype html>
<meta charset=utf-8>
<title>Should not scroll out of rounded corner</title>
<link rel="help" href="https://crbug.com/40277896">
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/dom/events/scrolling/scroll_support.js"></script>
<style>
#container {
width: 300px;
height: 300px;
border-radius: 100px;
overflow: hidden;
border: 2px solid blue;
}
#scroller {
overflow: auto;
width: 300px;
height: 300px;
will-change: scroll-position;
}
.spacer {
height: 200vh;
}
</style>
<div id="container">
<div id="scroller">
<div class="spacer"></div>
</div>
</div>
<div class="spacer"></div>
<script>
promise_test(async (t) => {
await waitForCompositorCommit();
let scrolled = new Promise((resolve) => {
let scrollers = [window, document.getElementById("scroller")];
let onscroll = (evt) => {
for (const scroller of scrollers) {
scroller.removeEventListener("scroll", onscroll);
}
resolve(evt.target.id || "root");
}
for (const scroller of scrollers) {
scroller.addEventListener("scroll", onscroll);
}
});
const actions = new test_driver.Actions().scroll(20, 20, 0, 50, { duration: 50 });
actions.send();
assert_equals(await scrolled, "root", "Incorrect element scrolled");
}, "Wheel-scroll out of rounded corner skips that scroller");
</script>

@ -0,0 +1,51 @@
<!doctype html>
<meta charset=utf-8>
<title>Should not scroll out of rounded corner</title>
<link rel="help" href="https://crbug.com/40277896">
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/dom/events/scrolling/scroll_support.js"></script>
<style>
#scroller {
border-radius: 100px;
overflow: auto;
width: 300px;
height: 300px;
border: 2px solid blue;
will-change: scroll-position;
}
.spacer {
height: 200vh;
}
</style>
<div id="scroller">
<div class="spacer"></div>
</div>
<div class="spacer"></div>
<script>
promise_test(async (t) => {
await waitForCompositorCommit();
let scrolled = new Promise((resolve) => {
let scrollers = [window, document.getElementById("scroller")];
let onscroll = (evt) => {
for (const scroller of scrollers) {
scroller.removeEventListener("scroll", onscroll);
}
resolve(evt.target.id || "root");
}
for (const scroller of scrollers) {
scroller.addEventListener("scroll", onscroll);
}
});
const actions = new test_driver.Actions().scroll(20, 20, 0, 50, { duration: 50 });
actions.send();
assert_equals(await scrolled, "root", "Incorrect element scrolled");
}, "Wheel-scroll out of rounded corner skips that scroller");
</script>