Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
@property(nonatomic, assign) double targetViewInsetBottom;
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;

/// VSyncClient for touch events delivery frame rate correction.
///
/// On promotion devices(eg: iPhone13 Pro), the delivery frame rate of touch events is 60HZ
/// but the frame rate of rendering is 120HZ, which is different and will leads jitter and laggy.
/// With this VSyncClient, it can correct the delivery frame rate of touch events to let it keep
/// the same with frame rate of rendering.
@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is VSyncClient necessary here? Since this is just for increasing resolution of touch events and doesn't directly affect rendering, should we just use CADisplayLink directly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using VSyncClient ensures the preferred frame rate that used is the same as the one used on UIThread.


/*
* Mouse and trackpad gesture recognizers
*/
Expand Down Expand Up @@ -680,6 +688,9 @@ - (void)viewDidLoad {
// Register internal plugins.
[self addInternalPlugins];

// Create a vsync client to correct delivery frame rate of touch events if needed.
[self createTouchRateCorrectionVSyncClientIfNeeded];

if (@available(iOS 13.4, *)) {
_hoverGestureRecognizer =
[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
Expand Down Expand Up @@ -842,6 +853,7 @@ - (void)dealloc {
[self deregisterNotifications];

[self invalidateKeyboardAnimationVSyncClient];
[self invalidateTouchRateCorrectionVSyncClient];
_scrollView.get().delegate = nil;
_hoverGestureRecognizer.delegate = nil;
[_hoverGestureRecognizer release];
Expand Down Expand Up @@ -975,6 +987,9 @@ - (void)dispatchTouches:(NSSet*)touches
}
}

// Activate or pause the correction of delivery frame rate of touch events.
[self triggerTouchRateCorrectionIfNeeded:touches];

const CGFloat scale = [UIScreen mainScreen].scale;
auto packet =
std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
Expand Down Expand Up @@ -1120,6 +1135,63 @@ - (void)forceTouchesCancelled:(NSSet*)touches {
[self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
}

#pragma mark - Touch events rate correction

- (void)createTouchRateCorrectionVSyncClientIfNeeded {
if (_touchRateCorrectionVSyncClient != nil) {
return;
}

double displayRefreshRate = [DisplayLinkManager displayRefreshRate];
const double epsilon = 0.1;
if (displayRefreshRate < 60.0 + epsilon) { // displayRefreshRate <= 60.0

// If current device's max frame rate is not larger than 60HZ, the delivery rate of touch events
// is the same with render vsync rate. So it is unnecessary to create
// _touchRateCorrectionVSyncClient to correct touch callback's rate.
return;
}

flutter::Shell& shell = [_engine.get() shell];
auto callback = [](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
// Do nothing in this block. Just trigger system to callback touch events with correct rate.
};
_touchRateCorrectionVSyncClient =
[[VSyncClient alloc] initWithTaskRunner:shell.GetTaskRunners().GetPlatformTaskRunner()
callback:callback];
_touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO;
}

- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches {
if (_touchRateCorrectionVSyncClient == nil) {
// If the _touchRateCorrectionVSyncClient is not created, means current devices doesn't
// need to correct the touch rate. So just return.
return;
}

// As long as there is a touch's phase is UITouchPhaseBegan or UITouchPhaseMoved,
// activate the correction. Otherwise pause the correction.
BOOL isUserInteracting = NO;
for (UITouch* touch in touches) {
if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
isUserInteracting = YES;
break;
}
}

if (isUserInteracting && [_engine.get() viewController] == self) {
[_touchRateCorrectionVSyncClient await];
} else {
[_touchRateCorrectionVSyncClient pause];
}
}

- (void)invalidateTouchRateCorrectionVSyncClient {
[_touchRateCorrectionVSyncClient invalidate];
[_touchRateCorrectionVSyncClient release];
_touchRateCorrectionVSyncClient = nil;
}

#pragma mark - Handle view resizing

- (void)updateViewportMetrics {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ @interface FlutterViewController (Tests)

@property(nonatomic, assign) double targetViewInsetBottom;

- (void)createTouchRateCorrectionVSyncClientIfNeeded;
- (void)surfaceUpdated:(BOOL)appeared;
- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
- (void)handlePressEvent:(FlutterUIPressProxy*)press
Expand Down Expand Up @@ -160,6 +161,18 @@ - (void)tearDown {
self.messageSent = nil;
}

- (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient {
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
[viewControllerMock loadView];
[viewControllerMock viewDidLoad];
OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]);
}

- (void)testStartKeyboardAnimationWillInvokeSetupKeyboardAnimationVsyncClient {
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

FLUTTER_ASSERT_NOT_ARC

@interface UITouch ()

@property(nonatomic, readwrite) UITouchPhase phase;

@end

@interface VSyncClient (Testing)

- (CADisplayLink*)getDisplayLink;
Expand All @@ -22,7 +28,11 @@ @interface FlutterViewController (Testing)
@property(nonatomic, assign) double targetViewInsetBottom;
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;

@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient;

- (void)createTouchRateCorrectionVSyncClientIfNeeded;
- (void)setupKeyboardAnimationVsyncClient;
- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches;

@end

Expand Down Expand Up @@ -56,4 +66,111 @@ - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterV
}
}

- (void)
testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient);
}

- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];

FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient;
XCTAssertNotNil(clientBefore);

[viewController createTouchRateCorrectionVSyncClientIfNeeded];
VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient;
XCTAssertNotNil(clientAfter);

XCTAssertTrue(clientBefore == clientAfter);
}

- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 60;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
}

- (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController loadView];
[viewController viewDidLoad];

VSyncClient* client = viewController.touchRateCorrectionVSyncClient;
CADisplayLink* link = [client getDisplayLink];

UITouch* fakeTouchBegan = [[UITouch alloc] init];
fakeTouchBegan.phase = UITouchPhaseBegan;

UITouch* fakeTouchMove = [[UITouch alloc] init];
fakeTouchMove.phase = UITouchPhaseMoved;

UITouch* fakeTouchEnd = [[UITouch alloc] init];
fakeTouchEnd.phase = UITouchPhaseEnded;

UITouch* fakeTouchCancelled = [[UITouch alloc] init];
fakeTouchCancelled.phase = UITouchPhaseCancelled;

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]];
XCTAssertFalse(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]];
XCTAssertTrue(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]];
XCTAssertFalse(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]];
XCTAssertTrue(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]];
XCTAssertFalse(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd,
fakeTouchCancelled, nil]];
XCTAssertTrue(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
initWithObjects:fakeTouchMove, fakeTouchEnd, nil]];
XCTAssertFalse(link.isPaused);
}

@end
19 changes: 19 additions & 0 deletions shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,23 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs
[vsyncClient release];
}

- (void)testAwaitAndPauseWillWorkCorrectly {
auto thread_task_runner = CreateNewThread("VsyncWaiterIosTest");
VSyncClient* vsyncClient = [[[VSyncClient alloc]
initWithTaskRunner:thread_task_runner
callback:[](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {}]
autorelease];

CADisplayLink* link = [vsyncClient getDisplayLink];
XCTAssertTrue(link.isPaused);

[vsyncClient await];
XCTAssertFalse(link.isPaused);

[vsyncClient pause];
XCTAssertTrue(link.isPaused);

[vsyncClient release];
}

@end
2 changes: 2 additions & 0 deletions shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

- (void)await;

- (void)pause;

- (void)invalidate;

- (double)getRefreshRate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ - (void)await {
display_link_.get().paused = NO;
}

- (void)pause {
display_link_.get().paused = YES;
}

- (void)onDisplayLink:(CADisplayLink*)link {
TRACE_EVENT0("flutter", "VSYNC");

Expand Down