Skip to content

Commit 6eea11a

Browse files
authored
[iOS] Extract FlutterVSyncClient from vsync_waiter_ios (#185737)
This refactoring cleans up VSyncClient's API, separating it into a clean, pure Objective-C API in FlutterVSyncClient.h, and a category that declares an initialiser with C++ types such as fml::TaskRunner and the C++ callback to be fired on vsync boundaries. To avoid hardcoding/redeclaring the Testing category that exposes `displayLink` and `onDisplayLink:` I've factored out a FlutterVSyncTesting+Testing.h which can be used in FlutterVSyncClientTest.mm and FlutterViewControllerTest.mm. This also renames VSyncClient and DisplayLinkManager to include the "Flutter" prefix, though the Swift names drop the prefix as we do with other classes. While initialisation of VSyncClient still requires C++ types, this refactoring allows users of VSyncClient such as FlutterKeyboardInsetManager and others to be implemented in Swift. Since this is used throughout the embedder, there's significant value in cleaning up the API, as it unblocks new code from needing to be written in Objective-C++. This is part of a series of refactorings that aim to place a thin, lightweight layer of abstraction between the core C++ engine and the iOS embedder. Core to this effort are VSyncWaiterIOS, PlatformViewIOS, and IOSExternalViewEmbedder. Background: flutter::VsyncWaiter is the core engine's common abstraction for a mechanism for waiting for and getting callbacks on frame boundaries vsync events. These callbacks are used by flutter::Animator to produce frames, but also for other purposes, including frame rate correction for touch events in FlutterViewController and syncing keyboard animations in FlutterKeyboardInsetManager. flutter::VsyncWaiterIOS is its iOS-specific concrete implementation. VsyncWaiterIOS allocates and owns an Objective-C VSyncClient object which is the core of the waiting mechanism. VSyncClient uses a CADisplayLink under the hood, via a probably extraneous DisplayLinkManager wrapper class. The general flow looks like: * On intialisation, VsyncWaiterIOS allocates and initialises a VSyncClient. It hands it a callback that should be called on vsync events, and a task runner whose run loop is used for setting up the CADisplayLink with the correct thread affinity (e.g. the UI thread when used to produce Flutter frames, and to manage insets during keyboard animations, or the platform thread for touch event rate correction). * core engine calls VsyncWaiter::AwaitVSync() to wait for the next frame boundary. VSyncWaiterIOS calls [VSyncClient await] which unpauses CADisplayLink so it starts sending vsync events. * On the next vsync from CADisplayLink [VSyncClient onDisplayLink:] is called which calculates high-precision frame_start_time and frame_target_time, pauses CADisplayLink again, then fires the vsync callback, notifying the core engine Follow-ups: After this refactoring, I have a follow-up patch that migrates this off the C++ fml::TaskRunner and flutter::TaskRunners types and onto the pure Objective-C FlutterFMLTaskRunner and FlutterFMLTaskRunners classes. Issue: #112232 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] These patches are Taylored to make migration to Swift a breeze. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 9eabb08 commit 6eea11a

14 files changed

Lines changed: 368 additions & 276 deletions

engine/src/flutter/shell/platform/darwin/ios/BUILD.gn

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,6 @@ source_set("flutter_framework_source") {
157157
"framework/Source/platform_message_response_darwin.mm",
158158
"framework/Source/profiler_metrics_ios.h",
159159
"framework/Source/profiler_metrics_ios.mm",
160-
"framework/Source/vsync_waiter_ios.h",
161-
"framework/Source/vsync_waiter_ios.mm",
162160
"ios_context.h",
163161
"ios_context.mm",
164162
"ios_context_metal_impeller.h",
@@ -310,12 +308,13 @@ if (enable_ios_unittests) {
310308
"framework/Source/FlutterTextureRegistryRelayTest.mm",
311309
"framework/Source/FlutterTouchInterceptingView_Test.h",
312310
"framework/Source/FlutterUndoManagerPluginTest.mm",
311+
"framework/Source/FlutterVSyncClient+Testing.h",
312+
"framework/Source/FlutterVSyncClientTest.mm",
313313
"framework/Source/FlutterViewControllerTest.mm",
314314
"framework/Source/FlutterViewTest.mm",
315315
"framework/Source/SemanticsObjectTest.mm",
316316
"framework/Source/SemanticsObjectTestMocks.h",
317317
"framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm",
318-
"framework/Source/VsyncWaiterIosTest.mm",
319318
"framework/Source/accessibility_bridge_test.mm",
320319
"framework/Source/availability_version_check_test.mm",
321320
"ios_context_noop_unittests.mm",
@@ -367,6 +366,11 @@ source_set("flutter_engine_bindings") {
367366
"framework/Source/FlutterFMLTaskRunner.mm",
368367
"framework/Source/FlutterFMLTaskRunners.h",
369368
"framework/Source/FlutterFMLTaskRunners.mm",
369+
"framework/Source/FlutterVSyncClient+FML.h",
370+
"framework/Source/FlutterVSyncClient.h",
371+
"framework/Source/FlutterVSyncClient.mm",
372+
"framework/Source/vsync_waiter_ios.h",
373+
"framework/Source/vsync_waiter_ios.mm",
370374
]
371375
deps = [
372376
"//flutter/common",

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardInsetManager.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
*
8282
* * When a keyboard notification (e.g., UIKeyboardWillShow) is received, the manager animates a
8383
* hidden UIView's frame using the native iOS duration and curve.
84-
* * A VSyncClient tracks the 'presentationLayer' of this hidden view on every vsync.
84+
* * A FlutterVSyncClient tracks the 'presentationLayer' of this hidden view on every vsync.
8585
* * The intermediate positions are then translated into physical pixel insets and sent to the
8686
* engine until the animation completes.
8787
*

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardInsetManager.mm

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardInsetManager.h"
66
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
7+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterVSyncClient+FML.h"
78
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
8-
#import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h"
99
#import "flutter/shell/platform/embedder/embedder.h"
1010
#import "flutter/third_party/spring_animation/spring_animation.h"
1111

@@ -20,7 +20,7 @@ @interface FlutterKeyboardInsetManager ()
2020
@property(nonatomic, weak) id<FlutterKeyboardInsetManagerDelegate> delegate;
2121
@property(nonatomic, assign, readwrite) CGFloat targetViewInsetBottom;
2222
@property(nonatomic, assign) CGFloat originalViewInsetBottom;
23-
@property(nonatomic, strong) VSyncClient* keyboardAnimationVSyncClient;
23+
@property(nonatomic, strong) FlutterVSyncClient* keyboardAnimationVSyncClient;
2424
@property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
2525
@property(nonatomic, assign) NSTimeInterval keyboardAnimationStartTime;
2626
@property(nonatomic, strong) UIView* keyboardAnimationView;
@@ -283,7 +283,7 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
283283
[self setUpKeyboardAnimationVsyncClient:^(NSTimeInterval targetTime) {
284284
[weakSelf handleKeyboardAnimationCallbackWithTargetTime:targetTime];
285285
}];
286-
VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient;
286+
FlutterVSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient;
287287

288288
[UIView animateWithDuration:duration
289289
animations:^{
@@ -403,7 +403,8 @@ - (void)setUpKeyboardAnimationVsyncClient:
403403

404404
id<FlutterKeyboardInsetManagerDelegate> delegate = self.delegate;
405405
_keyboardAnimationVSyncClient =
406-
[[VSyncClient alloc] initWithTaskRunner:delegate.engine.uiTaskRunner callback:uiCallback];
406+
[[FlutterVSyncClient alloc] initWithTaskRunner:delegate.engine.uiTaskRunner
407+
callback:uiCallback];
407408
_keyboardAnimationVSyncClient.allowPauseAfterVsync = NO;
408409
[_keyboardAnimationVSyncClient await];
409410
}

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
FLUTTER_ASSERT_ARC
1616

17-
@interface DisplayLinkManager : NSObject
17+
@interface FlutterDisplayLinkManager : NSObject
1818
@property(class, nonatomic, readonly) BOOL maxRefreshRateEnabledOnIPhone;
1919
+ (double)displayRefreshRate;
2020
@end
@@ -189,7 +189,7 @@ - (instancetype)init {
189189
FlutterMetalLayerDisplayLinkProxy* proxy =
190190
[[FlutterMetalLayerDisplayLinkProxy alloc] initWithLayer:self];
191191
_displayLink = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(onDisplayLink:)];
192-
[self setMaxRefreshRate:DisplayLinkManager.displayRefreshRate forceMax:NO];
192+
[self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:NO];
193193
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
194194
[[NSNotificationCenter defaultCenter] addObserver:self
195195
selector:@selector(didEnterBackground:)
@@ -209,7 +209,7 @@ - (void)setMaxRefreshRate:(double)refreshRate forceMax:(BOOL)forceMax {
209209
// thread which does not trigger actual core animation frame. As a workaround FlutterMetalLayer
210210
// has it's own displaylink scheduled on main thread, which is used to trigger core animation
211211
// frame allowing for 120hz updates.
212-
if (!DisplayLinkManager.maxRefreshRateEnabledOnIPhone) {
212+
if (!FlutterDisplayLinkManager.maxRefreshRateEnabledOnIPhone) {
213213
return;
214214
}
215215
double maxFrameRate = fmax(refreshRate, 60);
@@ -228,7 +228,7 @@ - (void)onDisplayLink:(CADisplayLink*)link {
228228
if (_displayLinkPauseCountdown == 3) {
229229
_displayLink.paused = YES;
230230
if (_displayLinkForcedMaxRate) {
231-
[self setMaxRefreshRate:DisplayLinkManager.displayRefreshRate forceMax:NO];
231+
[self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:NO];
232232
_displayLinkForcedMaxRate = NO;
233233
}
234234
} else {
@@ -426,7 +426,7 @@ - (void)presentOnMainThread:(FlutterTexture*)texture {
426426
_didSetContentsDuringThisDisplayLinkPeriod = YES;
427427
} else if (!_displayLinkForcedMaxRate) {
428428
_displayLinkForcedMaxRate = YES;
429-
[self setMaxRefreshRate:DisplayLinkManager.displayRefreshRate forceMax:YES];
429+
[self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:YES];
430430
}
431431
}
432432

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_FML_H_
6+
#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_FML_H_
7+
8+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterVSyncClient.h"
9+
10+
#include "flutter/fml/memory/ref_ptr.h"
11+
#include "flutter/fml/task_runner.h"
12+
#include "flutter/shell/common/vsync_waiter.h"
13+
14+
@interface FlutterVSyncClient ()
15+
16+
- (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
17+
callback:(flutter::VsyncWaiter::Callback)callback;
18+
19+
@end
20+
21+
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_FML_H_
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_TESTING_H_
6+
#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_TESTING_H_
7+
8+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterVSyncClient.h"
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
/**
13+
* Internal methods of FlutterVSyncClient for use in unit tests.
14+
*/
15+
@interface FlutterVSyncClient (Testing)
16+
17+
/**
18+
* The underlying CADisplayLink instance.
19+
*/
20+
@property(nonatomic, readonly) CADisplayLink* displayLink;
21+
22+
/**
23+
* Manually triggers the display link callback for testing without waiting for actual vsyncs.
24+
*/
25+
- (void)onDisplayLink:(CADisplayLink*)link;
26+
27+
@end
28+
29+
NS_ASSUME_NONNULL_END
30+
31+
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_TESTING_H_
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_H_
6+
#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_H_
7+
8+
#include <QuartzCore/CADisplayLink.h>
9+
10+
//------------------------------------------------------------------------------
11+
/// @brief Info.plist key enabling the full range of ProMotion refresh rates for CADisplayLink
12+
/// callbacks and CAAnimation animations in the app.
13+
///
14+
/// @see
15+
/// https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro#3885321
16+
///
17+
extern NSString* const kCADisableMinimumFrameDurationOnPhoneKey;
18+
19+
NS_SWIFT_NAME(DisplayLinkManager)
20+
@interface FlutterDisplayLinkManager : NSObject
21+
22+
//------------------------------------------------------------------------------
23+
/// @brief Whether the max refresh rate on iPhone ProMotion devices are enabled. This reflects
24+
/// the value of `CADisableMinimumFrameDurationOnPhone` in the info.plist file. On iPads
25+
/// that support ProMotion, the max refresh rate is always enabled.
26+
///
27+
/// @return YES if the max refresh rate on ProMotion devices is enabled.
28+
///
29+
@property(class, nonatomic, readonly) BOOL maxRefreshRateEnabledOnIPhone;
30+
31+
//------------------------------------------------------------------------------
32+
/// @brief The display refresh rate used for reporting purposes. The engine does not care
33+
/// about this for frame scheduling. It is only used by tools for instrumentation. The
34+
/// engine uses the duration field of the link per frame for frame scheduling.
35+
///
36+
/// @attention Do not use the this call in frame scheduling. It is only meant for reporting.
37+
///
38+
/// @return The refresh rate in frames per second.
39+
///
40+
@property(class, nonatomic, readonly) double displayRefreshRate;
41+
42+
@end
43+
44+
NS_SWIFT_NAME(VSyncClient)
45+
@interface FlutterVSyncClient : NSObject
46+
47+
//------------------------------------------------------------------------------
48+
/// @brief The current display refresh rate in Hertz, rounded to the nearest integer value. The
49+
/// value. This value is calculated during each vsync callback as the inverse of the
50+
/// frame duration (the time between the current frame and the target next frame). The
51+
/// resulting frequency is rounded to the nearest whole number to smooth out minor
52+
/// hardware timestamp variations.
53+
///
54+
@property(nonatomic, assign, readonly) double refreshRate;
55+
56+
//------------------------------------------------------------------------------
57+
/// @brief Default value is YES. Vsync client will pause vsync callback after receiving
58+
/// a vsync signal. Setting this property to NO can avoid this and vsync client
59+
/// will trigger vsync callback continuously.
60+
///
61+
///
62+
/// @param allowPauseAfterVsync Allow vsync client to pause after receiving a vsync signal.
63+
///
64+
@property(nonatomic, assign) BOOL allowPauseAfterVsync;
65+
66+
- (void)await;
67+
68+
- (void)pause;
69+
70+
//------------------------------------------------------------------------------
71+
/// @brief Call invalidate before releasing this object to remove from runloops.
72+
///
73+
- (void)invalidate;
74+
75+
- (void)setMaxRefreshRate:(double)refreshRate;
76+
77+
@end
78+
79+
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVSYNCCLIENT_H_

0 commit comments

Comments
 (0)