Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 1e74e9a

Browse files
authored
[ios][platform_view] fix platform view clipping path intersection (#54820)
It turns out that when there are multiple paths to clip, they are unioned together, rather than intersected. But clipping paths need to be intersected. There isn't any good way to intersect arbitrary paths. However, it is easy to intersect rect paths, which is the most common use case. Then we simply fallback to software rendering if we have to intersect non-rect paths. That is: **Case 1** Only 1 clipping path (either rect path or arbitrary path): Hardware rendering. This should be the most common use case **Case 2** Multiple rect clipping path: Hardware rendering. This is also common, and it's the linked issue that we are fixing. **Case 3** Other complex case (multiple non-rect clipping path) Fallback to software rendering. This should be rare. After #53826, we don't have a working benchmark that measures the main thread anymore. However, this PR shouldn't impact our ad benchmark, since it only has 1 clipping path. I will verify manually by checking Instruments and make sure no software rendering is happening. But we really should make the benchmark working again, not just for performance improvement, but also for monitoring regression. *List which issues are fixed by this PR. You must list at least one issue.* Fixes flutter/flutter#153904 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 709ec9a commit 1e74e9a

File tree

22 files changed

+1494
-61
lines changed

22 files changed

+1494
-61
lines changed

shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,6 +1856,102 @@ - (void)testClipRect {
18561856
}
18571857
}
18581858

1859+
- (void)testClipRect_multipleClips {
1860+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
1861+
1862+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1863+
/*platform=*/GetDefaultTaskRunner(),
1864+
/*raster=*/GetDefaultTaskRunner(),
1865+
/*ui=*/GetDefaultTaskRunner(),
1866+
/*io=*/GetDefaultTaskRunner());
1867+
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
1868+
flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner());
1869+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1870+
/*delegate=*/mock_delegate,
1871+
/*rendering_api=*/mock_delegate.settings_.enable_impeller
1872+
? flutter::IOSRenderingAPI::kMetal
1873+
: flutter::IOSRenderingAPI::kSoftware,
1874+
/*platform_views_controller=*/flutterPlatformViewsController,
1875+
/*task_runners=*/runners,
1876+
/*worker_task_runner=*/nil,
1877+
/*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
1878+
1879+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
1880+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
1881+
flutterPlatformViewsController->RegisterViewFactory(
1882+
factory, @"MockFlutterPlatformView",
1883+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
1884+
FlutterResult result = ^(id result) {
1885+
};
1886+
flutterPlatformViewsController->OnMethodCall(
1887+
[FlutterMethodCall
1888+
methodCallWithMethodName:@"create"
1889+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
1890+
result);
1891+
1892+
XCTAssertNotNil(gMockPlatformView);
1893+
1894+
UIView* flutterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
1895+
flutterPlatformViewsController->SetFlutterView(flutterView);
1896+
// Create embedded view params
1897+
flutter::MutatorsStack stack;
1898+
// Layer tree always pushes a screen scale factor to the stack
1899+
SkMatrix screenScaleMatrix =
1900+
SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
1901+
stack.PushTransform(screenScaleMatrix);
1902+
// Push a clip rect
1903+
SkRect rect1 = SkRect::MakeXYWH(2, 2, 3, 3);
1904+
stack.PushClipRect(rect1);
1905+
// Push another clip rect
1906+
SkRect rect2 = SkRect::MakeXYWH(3, 3, 3, 3);
1907+
stack.PushClipRect(rect2);
1908+
1909+
auto embeddedViewParams =
1910+
std::make_unique<flutter::EmbeddedViewParams>(screenScaleMatrix, SkSize::Make(10, 10), stack);
1911+
1912+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
1913+
flutterPlatformViewsController->CompositeWithParams(
1914+
2, flutterPlatformViewsController->GetCompositionParams(2));
1915+
1916+
gMockPlatformView.backgroundColor = UIColor.redColor;
1917+
XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]);
1918+
ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview;
1919+
[flutterView addSubview:childClippingView];
1920+
1921+
[flutterView setNeedsLayout];
1922+
[flutterView layoutIfNeeded];
1923+
1924+
/*
1925+
clip 1 clip 2
1926+
2 3 4 5 6 2 3 4 5 6
1927+
2 + - - + 2
1928+
3 | | 3 + - - +
1929+
4 | | 4 | |
1930+
5 + - - + 5 | |
1931+
6 6 + - - +
1932+
1933+
Result should be the intersection of 2 clips
1934+
2 3 4 5 6
1935+
2
1936+
3 + - +
1937+
4 | |
1938+
5 + - +
1939+
6
1940+
*/
1941+
CGRect insideClipping = CGRectMake(3, 3, 2, 2);
1942+
for (int i = 0; i < 10; i++) {
1943+
for (int j = 0; j < 10; j++) {
1944+
CGPoint point = CGPointMake(i, j);
1945+
int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:flutterView];
1946+
if (CGRectContainsPoint(insideClipping, point)) {
1947+
XCTAssertEqual(alpha, 255);
1948+
} else {
1949+
XCTAssertEqual(alpha, 0);
1950+
}
1951+
}
1952+
}
1953+
}
1954+
18591955
- (void)testClipRRect {
18601956
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
18611957

@@ -1959,6 +2055,126 @@ - (void)testClipRRect {
19592055
}
19602056
}
19612057

2058+
- (void)testClipRRect_multipleClips {
2059+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
2060+
2061+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2062+
/*platform=*/GetDefaultTaskRunner(),
2063+
/*raster=*/GetDefaultTaskRunner(),
2064+
/*ui=*/GetDefaultTaskRunner(),
2065+
/*io=*/GetDefaultTaskRunner());
2066+
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
2067+
flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner());
2068+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2069+
/*delegate=*/mock_delegate,
2070+
/*rendering_api=*/mock_delegate.settings_.enable_impeller
2071+
? flutter::IOSRenderingAPI::kMetal
2072+
: flutter::IOSRenderingAPI::kSoftware,
2073+
/*platform_views_controller=*/flutterPlatformViewsController,
2074+
/*task_runners=*/runners,
2075+
/*worker_task_runner=*/nil,
2076+
/*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
2077+
2078+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
2079+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
2080+
flutterPlatformViewsController->RegisterViewFactory(
2081+
factory, @"MockFlutterPlatformView",
2082+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
2083+
FlutterResult result = ^(id result) {
2084+
};
2085+
flutterPlatformViewsController->OnMethodCall(
2086+
[FlutterMethodCall
2087+
methodCallWithMethodName:@"create"
2088+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
2089+
result);
2090+
2091+
XCTAssertNotNil(gMockPlatformView);
2092+
2093+
UIView* flutterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
2094+
flutterPlatformViewsController->SetFlutterView(flutterView);
2095+
// Create embedded view params
2096+
flutter::MutatorsStack stack;
2097+
// Layer tree always pushes a screen scale factor to the stack
2098+
SkMatrix screenScaleMatrix =
2099+
SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
2100+
stack.PushTransform(screenScaleMatrix);
2101+
// Push a clip rrect
2102+
SkRRect rrect = SkRRect::MakeRectXY(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1);
2103+
stack.PushClipRRect(rrect);
2104+
// Push a clip rect
2105+
SkRect rect = SkRect::MakeXYWH(4, 2, 6, 6);
2106+
stack.PushClipRect(rect);
2107+
2108+
auto embeddedViewParams =
2109+
std::make_unique<flutter::EmbeddedViewParams>(screenScaleMatrix, SkSize::Make(10, 10), stack);
2110+
2111+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
2112+
flutterPlatformViewsController->CompositeWithParams(
2113+
2, flutterPlatformViewsController->GetCompositionParams(2));
2114+
2115+
gMockPlatformView.backgroundColor = UIColor.redColor;
2116+
XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]);
2117+
ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview;
2118+
[flutterView addSubview:childClippingView];
2119+
2120+
[flutterView setNeedsLayout];
2121+
[flutterView layoutIfNeeded];
2122+
2123+
/*
2124+
clip 1 clip 2
2125+
2 3 4 5 6 7 8 9 2 3 4 5 6 7 8 9
2126+
2 / - - - - \ 2 + - - - - +
2127+
3 | | 3 | |
2128+
4 | | 4 | |
2129+
5 | | 5 | |
2130+
6 | | 6 | |
2131+
7 \ - - - - / 7 + - - - - +
2132+
2133+
Result should be the intersection of 2 clips
2134+
2 3 4 5 6 7 8 9
2135+
2 + - - \
2136+
3 | |
2137+
4 | |
2138+
5 | |
2139+
6 | |
2140+
7 + - - /
2141+
*/
2142+
CGRect clipping = CGRectMake(4, 2, 4, 6);
2143+
for (int i = 0; i < 10; i++) {
2144+
for (int j = 0; j < 10; j++) {
2145+
CGPoint point = CGPointMake(i, j);
2146+
int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:flutterView];
2147+
if (i == 7 && (j == 2 || j == 7)) {
2148+
// Upper and lower right corners should be partially transparent.
2149+
XCTAssert(0 < alpha && alpha < 255);
2150+
} else if (
2151+
// left
2152+
(i == 4 && j >= 2 && j <= 7) ||
2153+
// right
2154+
(i == 7 && j >= 2 && j <= 7) ||
2155+
// top
2156+
(j == 2 && i >= 4 && i <= 7) ||
2157+
// bottom
2158+
(j == 7 && i >= 4 && i <= 7)) {
2159+
// Since we are falling back to software rendering for this case
2160+
// The edge pixels can be anti-aliased, so it may not be fully opaque.
2161+
XCTAssert(alpha > 127);
2162+
} else if ((i == 3 && j >= 1 && j <= 8) || (i == 8 && j >= 1 && j <= 8) ||
2163+
(j == 1 && i >= 3 && i <= 8) || (j == 8 && i >= 3 && i <= 8)) {
2164+
// Since we are falling back to software rendering for this case
2165+
// The edge pixels can be anti-aliased, so it may not be fully transparent.
2166+
XCTAssert(alpha < 127);
2167+
} else if (CGRectContainsPoint(clipping, point)) {
2168+
// Other pixels inside clipping should be fully opaque.
2169+
XCTAssertEqual(alpha, 255);
2170+
} else {
2171+
// Pixels outside clipping should be fully transparent.
2172+
XCTAssertEqual(alpha, 0);
2173+
}
2174+
}
2175+
}
2176+
}
2177+
19622178
- (void)testClipPath {
19632179
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
19642180

@@ -2063,6 +2279,127 @@ - (void)testClipPath {
20632279
}
20642280
}
20652281

2282+
- (void)testClipPath_multipleClips {
2283+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
2284+
2285+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2286+
/*platform=*/GetDefaultTaskRunner(),
2287+
/*raster=*/GetDefaultTaskRunner(),
2288+
/*ui=*/GetDefaultTaskRunner(),
2289+
/*io=*/GetDefaultTaskRunner());
2290+
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
2291+
flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner());
2292+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2293+
/*delegate=*/mock_delegate,
2294+
/*rendering_api=*/mock_delegate.settings_.enable_impeller
2295+
? flutter::IOSRenderingAPI::kMetal
2296+
: flutter::IOSRenderingAPI::kSoftware,
2297+
/*platform_views_controller=*/flutterPlatformViewsController,
2298+
/*task_runners=*/runners,
2299+
/*worker_task_runner=*/nil,
2300+
/*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
2301+
2302+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
2303+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
2304+
flutterPlatformViewsController->RegisterViewFactory(
2305+
factory, @"MockFlutterPlatformView",
2306+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
2307+
FlutterResult result = ^(id result) {
2308+
};
2309+
flutterPlatformViewsController->OnMethodCall(
2310+
[FlutterMethodCall
2311+
methodCallWithMethodName:@"create"
2312+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
2313+
result);
2314+
2315+
XCTAssertNotNil(gMockPlatformView);
2316+
2317+
UIView* flutterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
2318+
flutterPlatformViewsController->SetFlutterView(flutterView);
2319+
// Create embedded view params
2320+
flutter::MutatorsStack stack;
2321+
// Layer tree always pushes a screen scale factor to the stack
2322+
SkMatrix screenScaleMatrix =
2323+
SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
2324+
stack.PushTransform(screenScaleMatrix);
2325+
// Push a clip path
2326+
SkPath path;
2327+
path.addRoundRect(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1);
2328+
stack.PushClipPath(path);
2329+
// Push a clip rect
2330+
SkRect rect = SkRect::MakeXYWH(4, 2, 6, 6);
2331+
stack.PushClipRect(rect);
2332+
2333+
auto embeddedViewParams =
2334+
std::make_unique<flutter::EmbeddedViewParams>(screenScaleMatrix, SkSize::Make(10, 10), stack);
2335+
2336+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
2337+
flutterPlatformViewsController->CompositeWithParams(
2338+
2, flutterPlatformViewsController->GetCompositionParams(2));
2339+
2340+
gMockPlatformView.backgroundColor = UIColor.redColor;
2341+
XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]);
2342+
ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview;
2343+
[flutterView addSubview:childClippingView];
2344+
2345+
[flutterView setNeedsLayout];
2346+
[flutterView layoutIfNeeded];
2347+
2348+
/*
2349+
clip 1 clip 2
2350+
2 3 4 5 6 7 8 9 2 3 4 5 6 7 8 9
2351+
2 / - - - - \ 2 + - - - - +
2352+
3 | | 3 | |
2353+
4 | | 4 | |
2354+
5 | | 5 | |
2355+
6 | | 6 | |
2356+
7 \ - - - - / 7 + - - - - +
2357+
2358+
Result should be the intersection of 2 clips
2359+
2 3 4 5 6 7 8 9
2360+
2 + - - \
2361+
3 | |
2362+
4 | |
2363+
5 | |
2364+
6 | |
2365+
7 + - - /
2366+
*/
2367+
CGRect clipping = CGRectMake(4, 2, 4, 6);
2368+
for (int i = 0; i < 10; i++) {
2369+
for (int j = 0; j < 10; j++) {
2370+
CGPoint point = CGPointMake(i, j);
2371+
int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:flutterView];
2372+
if (i == 7 && (j == 2 || j == 7)) {
2373+
// Upper and lower right corners should be partially transparent.
2374+
XCTAssert(0 < alpha && alpha < 255);
2375+
} else if (
2376+
// left
2377+
(i == 4 && j >= 2 && j <= 7) ||
2378+
// right
2379+
(i == 7 && j >= 2 && j <= 7) ||
2380+
// top
2381+
(j == 2 && i >= 4 && i <= 7) ||
2382+
// bottom
2383+
(j == 7 && i >= 4 && i <= 7)) {
2384+
// Since we are falling back to software rendering for this case
2385+
// The edge pixels can be anti-aliased, so it may not be fully opaque.
2386+
XCTAssert(alpha > 127);
2387+
} else if ((i == 3 && j >= 1 && j <= 8) || (i == 8 && j >= 1 && j <= 8) ||
2388+
(j == 1 && i >= 3 && i <= 8) || (j == 8 && i >= 3 && i <= 8)) {
2389+
// Since we are falling back to software rendering for this case
2390+
// The edge pixels can be anti-aliased, so it may not be fully transparent.
2391+
XCTAssert(alpha < 127);
2392+
} else if (CGRectContainsPoint(clipping, point)) {
2393+
// Other pixels inside clipping should be fully opaque.
2394+
XCTAssertEqual(alpha, 255);
2395+
} else {
2396+
// Pixels outside clipping should be fully transparent.
2397+
XCTAssertEqual(alpha, 0);
2398+
}
2399+
}
2400+
}
2401+
}
2402+
20662403
- (void)testSetFlutterViewControllerAfterCreateCanStillDispatchTouchEvents {
20672404
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
20682405

0 commit comments

Comments
 (0)