Flutter macOS Embedder
FlutterDisplayLink.mm
Go to the documentation of this file.
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 
28 
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 
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