| Flutter macOS Embedder |

FlutterDisplayLink.mm
Go to the documentation of this file.
1 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
2
3 #include "flutter/fml/logging.h"
4
5 #include <algorithm>
6 #include <optional>
7 #include <thread>
8 #include <vector>
9
10 // Note on thread safety and locking:
11 //
12 // There are three mutexes used within the scope of this file:
13 // - CVDisplayLink internal mutex. This is locked during every CVDisplayLink method
14 // and is also held while display link calls the output handler.
15 // - DisplayLinkManager mutex.
16 // - _FlutterDisplayLink mutex (through @synchronized blocks).
17 //
18 // Special care must be taken to avoid deadlocks. Because CVDisplayLink holds the
19 // mutex for the entire duration of the output handler, it is necessary for
20 // DisplayLinkManager to not call any CVDisplayLink methods while holding its
21 // mutex. Instead it must retain the display link instance and then call the
22 // appropriate method with the mutex unlocked.
23 //
24 // Similarly _FlutterDisplayLink must not call any DisplayLinkManager methods
25 // within the @synchronized block.
26
27 @class _FlutterDisplayLinkView;
28
29 @interface _FlutterDisplayLink : FlutterDisplayLink {
30 _FlutterDisplayLinkView* _view;
31 std::optional<CGDirectDisplayID> _display_id;
32 BOOL _paused;
33 }
34
35 - (void)didFireWithTimestamp:(CFTimeInterval)timestamp
36 targetTimestamp:(CFTimeInterval)targetTimestamp;
37
38 @end
39
40 namespace {
41 class DisplayLinkManager {
42 public:
43 static DisplayLinkManager& Instance() {
44 static DisplayLinkManager instance;
45 return instance;
46 }
47
48 void UnregisterDisplayLink(_FlutterDisplayLink* display_link);
49 void RegisterDisplayLink(_FlutterDisplayLink* display_link, CGDirectDisplayID display_id);
50 void PausedDidChange(_FlutterDisplayLink* display_link);
51 CFTimeInterval GetNominalOutputPeriod(CGDirectDisplayID display_id);
52
53 private:
54 void OnDisplayLink(CVDisplayLinkRef display_link,
55 const CVTimeStamp* in_now,
56 const CVTimeStamp* in_output_time,
57 CVOptionFlags flags_in,
58 CVOptionFlags* flags_out);
59
60 struct ScreenEntry {
61 CGDirectDisplayID display_id;
62 std::vector<_FlutterDisplayLink*> clients;
63
64 /// Display link for this screen. It is not safe to call display link methods
65 /// on this object while holding the mutex. Instead the instance should be
66 /// retained, mutex unlocked and then released.
67 CVDisplayLinkRef display_link_locked;
68
69 bool ShouldBeRunning() {
70 return std::any_of(clients.begin(), clients.end(),
71 [](FlutterDisplayLink* link) { return !link.paused; });
72 }
73 };
74 std::vector<ScreenEntry> entries_;
75 std::mutex mutex_;
76 };
77
78 void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running) {
79 bool is_running = CVDisplayLinkIsRunning(display_link);
80 if (should_be_running && !is_running) {
81 if (CVDisplayLinkStart(display_link) == kCVReturnError) {
82 // CVDisplayLinkStart will fail if it was called from the display link thread.
83 // The problem is that it CVDisplayLinkStop doesn't clean the pthread_t value in the display
84 // link itself. If the display link is started and stopped before before the UI thread is
85 // started (*), pthread_self() of the UI thread may have same value as the one stored in
86 // CVDisplayLink. Because this can happen at most once starting the display link from a
87 // temporary thread is a reasonable workaround.
88 //
89 // (*) Display link is started before UI thread because FlutterVSyncWaiter will run display
90 // link for one tick at the beginning to determine vsync phase.
91 //
92 // http://www.openradar.me/radar?id=5520107644125184
93 CVDisplayLinkRef retained = CVDisplayLinkRetain(display_link);
94 [NSThread detachNewThreadWithBlock:^{
95 CVDisplayLinkStart(retained);
96 CVDisplayLinkRelease(retained);
97 }];
98 }
99 } else if (!should_be_running && is_running) {
100 CVDisplayLinkStop(display_link);
101 }
102 }
103
104 void DisplayLinkManager::UnregisterDisplayLink(_FlutterDisplayLink* display_link) {
105 std::unique_lock<std::mutex> lock(mutex_);
106 for (auto entry = entries_.begin(); entry != entries_.end(); ++entry) {
107 auto it = std::find(entry->clients.begin(), entry->clients.end(), display_link);
108 if (it != entry->clients.end()) {
109 entry->clients.erase(it);
110 if (entry->clients.empty()) {
111 // Erasing the entry - take the display link instance and stop / release it
112 // outside of the mutex.
113 CVDisplayLinkRef display_link = entry->display_link_locked;
114 entries_.erase(entry);
115 lock.unlock();
116 CVDisplayLinkStop(display_link);
117 CVDisplayLinkRelease(display_link);
118 } else {
119 // Update the display link state outside of the mutex.
120 bool should_be_running = entry->ShouldBeRunning();
121 CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry->display_link_locked);
122 lock.unlock();
123 RunOrStopDisplayLink(display_link, should_be_running);
124 CVDisplayLinkRelease(display_link);
125 }
126 return;
127 }
128 }
129 }
130
131 void DisplayLinkManager::RegisterDisplayLink(_FlutterDisplayLink* display_link,
132 CGDirectDisplayID display_id) {
133 std::unique_lock<std::mutex> lock(mutex_);
134 for (ScreenEntry& entry : entries_) {
135 if (entry.display_id == display_id) {
136 entry.clients.push_back(display_link);
137 bool should_be_running = entry.ShouldBeRunning();
138 CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry.display_link_locked);
139 lock.unlock();
140 RunOrStopDisplayLink(display_link, should_be_running);
141 CVDisplayLinkRelease(display_link);
142 return;
143 }
144 }
145
146 ScreenEntry entry;
147 entry.display_id = display_id;
148 entry.clients.push_back(display_link);
149 CVDisplayLinkCreateWithCGDisplay(display_id, &entry.display_link_locked);
150
151 CVDisplayLinkSetOutputHandler(
152 entry.display_link_locked,
153 ^(CVDisplayLinkRef display_link, const CVTimeStamp* in_now, const CVTimeStamp* in_output_time,
154 CVOptionFlags flags_in, CVOptionFlags* flags_out) {
155 OnDisplayLink(display_link, in_now, in_output_time, flags_in, flags_out);
156 return 0;
157 });
158
159 // This is a new display link so it is safe to start it with mutex held.
160 bool should_be_running = entry.ShouldBeRunning();
161 RunOrStopDisplayLink(entry.display_link_locked, should_be_running);
162 entries_.push_back(entry);
163 }
164
165 void DisplayLinkManager::PausedDidChange(_FlutterDisplayLink* display_link) {
166 std::unique_lock<std::mutex> lock(mutex_);
167 for (ScreenEntry& entry : entries_) {
168 auto it = std::find(entry.clients.begin(), entry.clients.end(), display_link);
169 if (it != entry.clients.end()) {
170 bool running = entry.ShouldBeRunning();
171 CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry.display_link_locked);
172 lock.unlock();
173 RunOrStopDisplayLink(display_link, running);
174 CVDisplayLinkRelease(display_link);
175 return;
176 }
177 }
178 }
179
180 CFTimeInterval DisplayLinkManager::GetNominalOutputPeriod(CGDirectDisplayID display_id) {
181 std::unique_lock<std::mutex> lock(mutex_);
182 for (ScreenEntry& entry : entries_) {
183 if (entry.display_id == display_id) {
184 CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry.display_link_locked);
185 lock.unlock();
186 CVTime latency = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link);
187 CVDisplayLinkRelease(display_link);
188 return (CFTimeInterval)latency.timeValue / (CFTimeInterval)latency.timeScale;
189 }
190 }
191 return 0;
192 }
193
194 void DisplayLinkManager::OnDisplayLink(CVDisplayLinkRef display_link,
195 const CVTimeStamp* in_now,
196 const CVTimeStamp* in_output_time,
197 CVOptionFlags flags_in,
198 CVOptionFlags* flags_out) {
199 // Hold the mutex only while copying clients.
200 std::vector<_FlutterDisplayLink*> clients;
201 {
202 std::lock_guard<std::mutex> lock(mutex_);
203 for (ScreenEntry& entry : entries_) {
204 if (entry.display_link_locked == display_link) {
205 clients = entry.clients;
206 break;
207 }
208 }
209 }
210
211 CFTimeInterval timestamp = (CFTimeInterval)in_now->hostTime / CVGetHostClockFrequency();
212 CFTimeInterval target_timestamp =
213 (CFTimeInterval)in_output_time->hostTime / CVGetHostClockFrequency();
214
215 for (_FlutterDisplayLink* client : clients) {
216 [client didFireWithTimestamp:timestamp targetTimestamp:target_timestamp];
217 }
218 }
219 } // namespace
220
221 @interface _FlutterDisplayLinkView : NSView {
222 }
223
224 @end
225
226 static NSString* const kFlutterDisplayLinkViewDidMoveToWindow =
227 @"FlutterDisplayLinkViewDidMoveToWindow";
228
229 @implementation _FlutterDisplayLinkView
230
231 - (void)viewDidMoveToWindow {
232 [super viewDidMoveToWindow];
233 [[NSNotificationCenter defaultCenter] postNotificationName:kFlutterDisplayLinkViewDidMoveToWindow
234 object:self];
235 }
236
237 @end
238
239 @implementation _FlutterDisplayLink
240
241 @synthesize delegate = _delegate;
242
243 - (instancetype)initWithView:(NSView*)view {
244 FML_DCHECK([NSThread isMainThread]);
245 if (self = [super init]) {
246 self->_view = [[_FlutterDisplayLinkView alloc] initWithFrame:CGRectZero];
247 [view addSubview:self->_view];
248 _paused = YES;
249 [[NSNotificationCenter defaultCenter] addObserver:self
250 selector:@selector(viewDidChangeWindow:)
251 name:kFlutterDisplayLinkViewDidMoveToWindow
252 object:self->_view];
253 [[NSNotificationCenter defaultCenter] addObserver:self
254 selector:@selector(windowDidChangeScreen:)
255 name:NSWindowDidChangeScreenNotification
256 object:nil];
257 [self updateScreen];
258 }
259 return self;
260 }
261
262 - (void)invalidate {
263 @synchronized(self) {
264 FML_DCHECK([NSThread isMainThread]);
265 // Unregister observer before removing the view to ensure
266 // that the viewDidChangeWindow notification is not received
267 // while in @synchronized block.
268 [[NSNotificationCenter defaultCenter] removeObserver:self];
269 [_view removeFromSuperview];
270 _view = nil;
271 _delegate = nil;
272 }
273 DisplayLinkManager::Instance().UnregisterDisplayLink(self);
274 }
275
276 - (void)updateScreen {
277 DisplayLinkManager::Instance().UnregisterDisplayLink(self);
278 std::optional<CGDirectDisplayID> displayId;
279 @synchronized(self) {
280 NSScreen* screen = _view.window.screen;
281 if (screen != nil) {
282 // https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc
283 _display_id = (CGDirectDisplayID)[
284 [[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
285 } else {
286 _display_id = std::nullopt;
287 }
288 displayId = _display_id;
289 }
290 if (displayId.has_value()) {
291 DisplayLinkManager::Instance().RegisterDisplayLink(self, *displayId);
292 }
293 }
294
295 - (void)viewDidChangeWindow:(NSNotification*)notification {
296 NSView* view = notification.object;
297 if (_view == view) {
298 [self updateScreen];
299 }
300 }
301
302 - (void)windowDidChangeScreen:(NSNotification*)notification {
303 NSWindow* window = notification.object;
304 if (_view.window == window) {
305 [self updateScreen];
306 }
307 }
308
309 - (void)didFireWithTimestamp:(CFTimeInterval)timestamp
310 targetTimestamp:(CFTimeInterval)targetTimestamp {
311 @synchronized(self) {
312 if (!_paused) {
313 id<FlutterDisplayLinkDelegate> delegate = _delegate;
314 [delegate onDisplayLink:timestamp targetTimestamp:targetTimestamp];
315 }
316 }
317 }
318
319 - (BOOL)paused {
320 @synchronized(self) {
321 return _paused;
322 }
323 }
324
325 - (void)setPaused:(BOOL)paused {
326 @synchronized(self) {
327 if (_paused == paused) {
328 return;
329 }
330 _paused = paused;
331 }
332 DisplayLinkManager::Instance().PausedDidChange(self);
333 }
334
335 - (CFTimeInterval)nominalOutputRefreshPeriod {
336 CGDirectDisplayID display_id;
337 @synchronized(self) {
338 if (_display_id.has_value()) {
339 display_id = *_display_id;
340 } else {
341 return 0;
342 }
343 }
344 return DisplayLinkManager::Instance().GetNominalOutputPeriod(display_id);
345 }
346
347 @end
348
349 @implementation FlutterDisplayLink
350 + (instancetype)displayLinkWithView:(NSView*)view {
351 return [[_FlutterDisplayLink alloc] initWithView:view];
352 }
353
354 - (void)invalidate {
355 [self doesNotRecognizeSelector:_cmd];
356 }
357
358 @end
FlutterDisplayLink.h
kFlutterDisplayLinkViewDidMoveToWindow
static NSString *const kFlutterDisplayLinkViewDidMoveToWindow
Definition: FlutterDisplayLink.mm:226
_FlutterDisplayLink
Definition: FlutterDisplayLink.mm:29
_FlutterDisplayLink::_display_id
std::optional< CGDirectDisplayID > _display_id
Definition: FlutterDisplayLink.mm:31
_FlutterDisplayLink::_view
_FlutterDisplayLinkView * _view
Definition: FlutterDisplayLink.mm:30
_FlutterDisplayLink::_paused
BOOL _paused
Definition: FlutterDisplayLink.mm:32
_FlutterDisplayLinkView
Definition: FlutterDisplayLink.mm:221
FlutterDisplayLink
Definition: FlutterDisplayLink.h:16
-[FlutterDisplayLink invalidate]
void invalidate()
Invalidates the display link. Must be called on the main thread.
Definition: FlutterDisplayLink.mm:354
FlutterDisplayLink::nominalOutputRefreshPeriod
CFTimeInterval nominalOutputRefreshPeriod
Definition: FlutterDisplayLink.h:33
FlutterDisplayLink::paused
BOOL paused
Pauses and resumes the display link. May be called from any thread.
Definition: FlutterDisplayLink.h:28
FlutterDisplayLink::delegate
id< FlutterDisplayLinkDelegate > delegate
Definition: FlutterDisplayLink.h:25