| Flutter iOS Embedder |

vsync_waiter_ios.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
5 #import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h"
6
7 #include <utility>
8
9 #include <Foundation/Foundation.h>
10 #include <UIKit/UIKit.h>
11 #include <mach/mach_time.h>
12
13 #include "flutter/common/task_runners.h"
14 #include "flutter/fml/logging.h"
15 #include "flutter/fml/memory/task_runner_checker.h"
16 #include "flutter/fml/trace_event.h"
17
18 // When calculating refresh rate diffrence, anything within 0.1 fps is ignored.
19 const static double kRefreshRateDiffToIgnore = 0.1;
20
21 namespace flutter {
22
23 VsyncWaiterIOS::VsyncWaiterIOS(const flutter::TaskRunners& task_runners)
24 : VsyncWaiter(task_runners) {
25 auto callback = [this](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
26 const fml::TimePoint start_time = recorder->GetVsyncStartTime();
27 const fml::TimePoint target_time = recorder->GetVsyncTargetTime();
28 FireCallback(start_time, target_time, true);
29 };
30 client_ =
31 fml::scoped_nsobject{[[VSyncClient alloc] initWithTaskRunner:task_runners_.GetUITaskRunner()
32 callback:callback]};
33 max_refresh_rate_ = [DisplayLinkManager displayRefreshRate];
34 }
35
36 VsyncWaiterIOS::~VsyncWaiterIOS() {
37 // This way, we will get no more callbacks from the display link that holds a weak (non-nilling)
38 // reference to this C++ object.
39 [client_.get() invalidate];
40 }
41
42 void VsyncWaiterIOS::AwaitVSync() {
43 double new_max_refresh_rate = [DisplayLinkManager displayRefreshRate];
44 if (fml::TaskRunnerChecker::RunsOnTheSameThread(
45 task_runners_.GetRasterTaskRunner()->GetTaskQueueId(),
46 task_runners_.GetPlatformTaskRunner()->GetTaskQueueId())) {
47 BOOL isRunningOnMac = NO;
48 if (@available(iOS 14.0, *)) {
49 isRunningOnMac = [NSProcessInfo processInfo].iOSAppOnMac;
50 }
51 if (!isRunningOnMac) {
52 // Pressure tested on iPhone 13 pro, the oldest iPhone that supports refresh rate greater than
53 // 60fps. A flutter app can handle fast scrolling on 80 fps with 6 PlatformViews in the scene
54 // at the same time.
55 new_max_refresh_rate = 80;
56 }
57 }
58 if (fabs(new_max_refresh_rate - max_refresh_rate_) > kRefreshRateDiffToIgnore) {
59 max_refresh_rate_ = new_max_refresh_rate;
60 [client_.get() setMaxRefreshRate:max_refresh_rate_];
61 }
62 [client_.get() await];
63 }
64
65 // |VariableRefreshRateReporter|
66 double VsyncWaiterIOS::GetRefreshRate() const {
67 return [client_.get() getRefreshRate];
68 }
69
70 fml::scoped_nsobject<VSyncClient> VsyncWaiterIOS::GetVsyncClient() const {
71 return client_;
72 }
73
74 } // namespace flutter
75
76 @implementation VSyncClient {
77 flutter::VsyncWaiter::Callback callback_;
78 fml::scoped_nsobject<CADisplayLink> display_link_;
79 double current_refresh_rate_;
80 }
81
82 - (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
83 callback:(flutter::VsyncWaiter::Callback)callback {
84 self = [super init];
85
86 if (self) {
87 current_refresh_rate_ = [DisplayLinkManager displayRefreshRate];
88 _allowPauseAfterVsync = YES;
89 callback_ = std::move(callback);
90 display_link_ = fml::scoped_nsobject<CADisplayLink> {
91 [[CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)] retain]
92 };
93 display_link_.get().paused = YES;
94
95 [self setMaxRefreshRate:[DisplayLinkManager displayRefreshRate]];
96
97 task_runner->PostTask([client = [self retain]]() {
98 [client->display_link_.get() addToRunLoop:[NSRunLoop currentRunLoop]
99 forMode:NSRunLoopCommonModes];
100 [client release];
101 });
102 }
103
104 return self;
105 }
106
107 - (void)setMaxRefreshRate:(double)refreshRate {
108 if (!DisplayLinkManager.maxRefreshRateEnabledOnIPhone) {
109 return;
110 }
111 double maxFrameRate = fmax(refreshRate, 60);
112 double minFrameRate = fmax(maxFrameRate / 2, 60);
113 if (@available(iOS 15.0, *)) {
114 display_link_.get().preferredFrameRateRange =
115 CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate);
116 } else {
117 display_link_.get().preferredFramesPerSecond = maxFrameRate;
118 }
119 }
120
121 - (void)await {
122 display_link_.get().paused = NO;
123 }
124
125 - (void)pause {
126 display_link_.get().paused = YES;
127 }
128
129 - (void)onDisplayLink:(CADisplayLink*)link {
130 CFTimeInterval delay = CACurrentMediaTime() - link.timestamp;
131 fml::TimePoint frame_start_time = fml::TimePoint::Now() - fml::TimeDelta::FromSecondsF(delay);
132
133 CFTimeInterval duration = link.targetTimestamp - link.timestamp;
134 fml::TimePoint frame_target_time = frame_start_time + fml::TimeDelta::FromSecondsF(duration);
135
136 TRACE_EVENT2_INT("flutter", "PlatformVsync", "frame_start_time",
137 frame_start_time.ToEpochDelta().ToMicroseconds(), "frame_target_time",
138 frame_target_time.ToEpochDelta().ToMicroseconds());
139
140 std::unique_ptr<flutter::FrameTimingsRecorder> recorder =
141 std::make_unique<flutter::FrameTimingsRecorder>();
142
143 current_refresh_rate_ = round(1 / (frame_target_time - frame_start_time).ToSecondsF());
144
145 recorder->RecordVsync(frame_start_time, frame_target_time);
146 if (_allowPauseAfterVsync) {
147 display_link_.get().paused = YES;
148 }
149 callback_(std::move(recorder));
150 }
151
152 - (void)invalidate {
153 [display_link_.get() invalidate];
154 }
155
156 - (void)dealloc {
157 [self invalidate];
158
159 [super dealloc];
160 }
161
162 - (double)getRefreshRate {
163 return current_refresh_rate_;
164 }
165
166 - (CADisplayLink*)getDisplayLink {
167 return display_link_.get();
168 }
169
170 @end
171
172 @implementation DisplayLinkManager
173
174 + (double)displayRefreshRate {
175 fml::scoped_nsobject<CADisplayLink> display_link = fml::scoped_nsobject<CADisplayLink> {
176 [[CADisplayLink displayLinkWithTarget:[[[DisplayLinkManager alloc] init] autorelease]
177 selector:@selector(onDisplayLink:)] retain]
178 };
179 display_link.get().paused = YES;
180 auto preferredFPS = display_link.get().preferredFramesPerSecond;
181
182 // From Docs:
183 // The default value for preferredFramesPerSecond is 0. When this value is 0, the preferred
184 // frame rate is equal to the maximum refresh rate of the display, as indicated by the
185 // maximumFramesPerSecond property.
186
187 if (preferredFPS != 0) {
188 return preferredFPS;
189 }
190
191 return [UIScreen mainScreen].maximumFramesPerSecond;
192 }
193
194 - (void)onDisplayLink:(CADisplayLink*)link {
195 // no-op.
196 }
197
198 + (BOOL)maxRefreshRateEnabledOnIPhone {
199 return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]
200 boolValue];
201 }
202
203 @end
kRefreshRateDiffToIgnore
const static double kRefreshRateDiffToIgnore
Definition: vsync_waiter_ios.mm:19
-[flutter::VsyncWaiterIOS AwaitVSync]
void AwaitVSync() override
Definition: vsync_waiter_ios.mm:42
DisplayLinkManager::maxRefreshRateEnabledOnIPhone
BOOL maxRefreshRateEnabledOnIPhone
Definition: FlutterMetalLayer.mm:13
-[flutter::VsyncWaiterIOS GetRefreshRate]
double GetRefreshRate() const override
Definition: vsync_waiter_ios.mm:66
DisplayLinkManager
Definition: FlutterMetalLayer.mm:12
-[flutter::VsyncWaiterIOS ~VsyncWaiterIOS]
~VsyncWaiterIOS() override
Definition: vsync_waiter_ios.mm:36
display_link_
fml::scoped_nsobject< CADisplayLink > display_link_
Definition: vsync_waiter_ios.mm:76
-[VSyncClient setMaxRefreshRate:]
void setMaxRefreshRate:(double refreshRate)
Definition: vsync_waiter_ios.mm:107
current_refresh_rate_
double current_refresh_rate_
Definition: vsync_waiter_ios.mm:79
flutter
Definition: accessibility_bridge.h:28
-[flutter::VsyncWaiterIOS GetVsyncClient]
fml::scoped_nsobject< VSyncClient > GetVsyncClient() const
Definition: vsync_waiter_ios.mm:70
-[flutter::VsyncWaiterIOS VsyncWaiterIOS]
VsyncWaiterIOS(const flutter::TaskRunners &task_runners)
Definition: vsync_waiter_ios.mm:23
vsync_waiter_ios.h
+[DisplayLinkManager displayRefreshRate]
double displayRefreshRate()
Definition: vsync_waiter_ios.mm:174
VSyncClient
Definition: vsync_waiter_ios.h:38