Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
FlutterVSyncClient.mm
Go to the documentation of this file.
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
6
7#import <UIKit/UIKit.h>
8
12
14
15NSString* const kCADisableMinimumFrameDurationOnPhoneKey = @"CADisableMinimumFrameDurationOnPhone";
16static const double kDefaultRefreshRate = 60.0;
17
18@implementation FlutterVSyncClient {
19 void (^_callback)(CFTimeInterval startTime, CFTimeInterval targetTime);
20 CADisplayLink* _displayLink;
22}
23
24- (instancetype)initWithTaskRunner:(FlutterFMLTaskRunner*)taskRunner
25 isVariableRefreshRateEnabled:(BOOL)isVariableRefreshRateEnabled
26 maxRefreshRate:(double)maxRefreshRate
27 callback:(void (^)(CFTimeInterval startTime,
28 CFTimeInterval targetTime))callback {
29 NSAssert(callback, @"callback must not be nil");
30 NSAssert(taskRunner, @"taskRunner must not be nil");
31
32 if (self = [super init]) {
33 _refreshRate = maxRefreshRate;
34 _isVariableRefreshRateEnabled = isVariableRefreshRateEnabled;
35 _allowPauseAfterVsync = YES;
36 _callback = callback;
37 _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)];
38 _displayLink.paused = YES;
39
40 [self setMaxRefreshRate:maxRefreshRate];
41
42 // Capture a weak reference to self to ensure we don't add the display link
43 // to the run loop if the client has already been deallocated.
44 __weak FlutterVSyncClient* weakSelf = self;
45 [taskRunner postTask:^{
46 FlutterVSyncClient* strongSelf = weakSelf;
47 if (strongSelf) {
48 [strongSelf.displayLink addToRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes];
49 }
50 }];
51 }
52
53 return self;
54}
55
56- (void)setMaxRefreshRate:(double)refreshRate {
58 return;
59 }
60 double maxFrameRate = fmax(refreshRate, kDefaultRefreshRate);
61 double minFrameRate = fmax(maxFrameRate / 2, kDefaultRefreshRate);
62 if (@available(iOS 15.0, *)) {
63 _displayLink.preferredFrameRateRange =
64 CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate);
65 } else {
66 _displayLink.preferredFramesPerSecond = maxFrameRate;
67 }
68}
69
70- (void)await {
71 _displayLink.paused = NO;
72}
73
74- (void)pause {
75 _displayLink.paused = YES;
76}
77
78- (void)onDisplayLink:(CADisplayLink*)link {
79 // CADisplayLink timestamps use the CACurrentMediaTime() monotonic clock (seconds since boot).
80 // CACurrentMediaTime() is based on mach_absolute_time, whereas the core engine uses
81 // fml::TimePoint, which is implemented with std::chrono::steady_clock, which uses
82 // mach_continuous_time under the hood. Thus, the values passed to the engine in the vsync
83 // callback need to be rebased to fml::TimePoint's epoch.
84 //
85 // According to Apple's docs, before the first frame is delivered, or when the display link is
86 // paused, both timestamp and targetTimestamp properties are 0.0. To guarantee consistent frame
87 // progression, we guard against zero values and fall back to CACurrentMediaTime().
88 CFTimeInterval timestamp = link.timestamp;
89 if (timestamp == 0.0) {
90 timestamp = CACurrentMediaTime();
91 }
92
93 // targetTimestamp is the anticipated presentation time of the next screen refresh. If
94 // targetTimestamp is zero or less than/equal to timestamp (which also occurs on paused/unpaused
95 // transitions), synthesize a projected target time based on the current refresh rate.
96 CFTimeInterval targetTimestamp = link.targetTimestamp;
97 if (targetTimestamp <= timestamp) {
98 double effectiveRefreshRate = _refreshRate > 0.0 ? _refreshRate : kDefaultRefreshRate;
99 targetTimestamp = timestamp + (1.0 / effectiveRefreshRate);
100 }
101 CFTimeInterval duration = targetTimestamp - timestamp;
102
104
105 // In steady-state, duration reflects the hardware refresh interval (e.g., ~0.01667s for 60Hz).
106 // We dynamically recalculate the refresh rate from the frame duration to adjust to ProMotion
107 // display refresh rate shifts.
108 //
109 // Round to nearest whole Hz value to ensure we don't introduce frame timing issues due to
110 // floating point error. e.g. 59.998, 60.004, 59.995, ... --> 60.000, 60.000, 60.000, ...
111 if (duration > 0) {
112 double roundedRefreshRate = round(1.0 / duration);
113 if (roundedRefreshRate > 0.0) {
114 _refreshRate = roundedRefreshRate;
115 }
116 }
117
118 if (_allowPauseAfterVsync) {
119 link.paused = YES;
120 }
121 _callback(timestamp, targetTimestamp);
122}
123
124- (void)dealloc {
125 [self invalidate];
126}
127
128- (void)invalidate {
129 [_displayLink invalidate];
130 _displayLink = nil;
131}
132
133- (CADisplayLink*)displayLink {
134 return _displayLink;
135}
136
137@end
138
139@implementation FlutterDisplayLinkManager
140
141+ (double)displayRefreshRate {
142 // TODO(cbracken): This code is incorrect. https://github.com/flutter/flutter/issues/185759
143 //
144 // We create a new CADisplayLink, call `preferredFramesPerSecond` on it, then immediately throw it
145 // away. As noted below, the default value for `preferredFramesPerSecond` is zero, in which case,
146 // we just return UIScreen.mainScreen.maximumFramesPerSecond in all cases; everything before that
147 // line can be deleted.
148 //
149 // If we intend to support configurable preferred FPS, then we should provide API for it. We
150 // should delete this code either way.
151
152 CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:[[[self class] alloc] init]
153 selector:@selector(onDisplayLink:)];
154 displayLink.paused = YES;
155 NSInteger preferredFPS = displayLink.preferredFramesPerSecond;
156
157 // From Docs:
158 // The default value for preferredFramesPerSecond is 0. When this value is 0, the preferred
159 // frame rate is equal to the maximum refresh rate of the display, as indicated by the
160 // maximumFramesPerSecond property.
161
162 if (preferredFPS != 0) {
163 return preferredFPS;
164 }
165
166 return UIScreen.mainScreen.maximumFramesPerSecond;
167}
168
169- (void)onDisplayLink:(CADisplayLink*)link {
170 // no-op.
171}
172
174 return [[NSBundle.mainBundle objectForInfoDictionaryKey:kCADisableMinimumFrameDurationOnPhoneKey]
175 boolValue];
176}
177
178@end
CADisplayLink * _displayLink
BOOL _isVariableRefreshRateEnabled
FLUTTER_ASSERT_ARC NSString *const kCADisableMinimumFrameDurationOnPhoneKey
Info.plist key enabling the full range of ProMotion refresh rates for CADisplayLink callbacks and CAA...
static const double kDefaultRefreshRate
FlutterDesktopBinaryReply callback
void postTask:(void(^ task)(void))
void tracePlatformVsyncWithStartTime:targetTime:(NSTimeInterval startTime,[targetTime] NSTimeInterval targetTime)
A client that wraps a CADisplayLink to deliver synchronized vsync signals.
int BOOL