Flutter macOS Embedder
FlutterDisplayLink.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.
5 
6 #include <algorithm>
7 #include <mutex>
8 #include <optional>
9 #include <thread>
10 #include <vector>
11 
12 #include "flutter/fml/logging.h"
13 #import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.h"
14 
16 
19  std::optional<CGDirectDisplayID> _display_id;
20  BOOL _paused;
21 }
22 
23 - (void)didFireWithTimestamp:(CFTimeInterval)timestamp
24  targetTimestamp:(CFTimeInterval)targetTimestamp;
25 
26 @end
27 
28 namespace {
29 class DisplayLinkManager {
30  public:
31  static DisplayLinkManager& Instance() {
32  static DisplayLinkManager instance;
33  return instance;
34  }
35 
36  void UnregisterDisplayLink(_FlutterDisplayLink* display_link);
37  void RegisterDisplayLink(_FlutterDisplayLink* display_link, CGDirectDisplayID display_id);
38  void PausedDidChange(_FlutterDisplayLink* display_link);
39  CFTimeInterval GetNominalOutputPeriod(CGDirectDisplayID display_id);
40 
41  private:
42  void OnDisplayLink(CVDisplayLinkRef display_link,
43  const CVTimeStamp* in_now,
44  const CVTimeStamp* in_output_time,
45  CVOptionFlags flags_in,
46  CVOptionFlags* flags_out);
47 
48  struct ScreenEntry {
49  CGDirectDisplayID display_id;
50  std::vector<_FlutterDisplayLink*> clients;
51  CVDisplayLinkRef display_link;
52 
53  bool ShouldBeRunning() {
54  return std::any_of(clients.begin(), clients.end(),
55  [](FlutterDisplayLink* link) { return !link.paused; });
56  }
57  };
58  std::vector<ScreenEntry> entries_;
59 };
60 
61 void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running) {
62  bool is_running = CVDisplayLinkIsRunning(display_link);
63  if (should_be_running && !is_running) {
64  CVDisplayLinkStart(display_link);
65  } else if (!should_be_running && is_running) {
66  CVDisplayLinkStop(display_link);
67  }
68 }
69 
70 void DisplayLinkManager::UnregisterDisplayLink(_FlutterDisplayLink* display_link) {
71  FML_DCHECK(NSThread.isMainThread);
72  for (auto entry = entries_.begin(); entry != entries_.end(); ++entry) {
73  auto it = std::find(entry->clients.begin(), entry->clients.end(), display_link);
74  if (it != entry->clients.end()) {
75  entry->clients.erase(it);
76  if (entry->clients.empty()) {
77  // Erasing the entry - take the display link instance and stop / release it
78  // outside of the mutex.
79  CVDisplayLinkStop(entry->display_link);
80  CVDisplayLinkRelease(entry->display_link);
81  entries_.erase(entry);
82  } else {
83  // Update the display link state outside of the mutex.
84  RunOrStopDisplayLink(entry->display_link, entry->ShouldBeRunning());
85  }
86  return;
87  }
88  }
89 }
90 
91 void DisplayLinkManager::RegisterDisplayLink(_FlutterDisplayLink* display_link,
92  CGDirectDisplayID display_id) {
93  FML_DCHECK(NSThread.isMainThread);
94  for (ScreenEntry& entry : entries_) {
95  if (entry.display_id == display_id) {
96  entry.clients.push_back(display_link);
97  RunOrStopDisplayLink(entry.display_link, entry.ShouldBeRunning());
98  return;
99  }
100  }
101 
102  ScreenEntry entry;
103  entry.display_id = display_id;
104  entry.clients.push_back(display_link);
105  CVDisplayLinkCreateWithCGDisplay(display_id, &entry.display_link);
106 
107  CVDisplayLinkSetOutputHandler(
108  entry.display_link,
109  ^(CVDisplayLinkRef display_link, const CVTimeStamp* in_now, const CVTimeStamp* in_output_time,
110  CVOptionFlags flags_in, CVOptionFlags* flags_out) {
111  OnDisplayLink(display_link, in_now, in_output_time, flags_in, flags_out);
112  return 0;
113  });
114 
115  // This is a new display link so it is safe to start it with mutex held.
116  RunOrStopDisplayLink(entry.display_link, entry.ShouldBeRunning());
117  entries_.push_back(entry);
118 }
119 
120 void DisplayLinkManager::PausedDidChange(_FlutterDisplayLink* display_link) {
121  for (ScreenEntry& entry : entries_) {
122  auto it = std::find(entry.clients.begin(), entry.clients.end(), display_link);
123  if (it != entry.clients.end()) {
124  RunOrStopDisplayLink(entry.display_link, entry.ShouldBeRunning());
125  return;
126  }
127  }
128 }
129 
130 CFTimeInterval DisplayLinkManager::GetNominalOutputPeriod(CGDirectDisplayID display_id) {
131  for (ScreenEntry& entry : entries_) {
132  if (entry.display_id == display_id) {
133  CVTime latency = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(entry.display_link);
134  return (CFTimeInterval)latency.timeValue / (CFTimeInterval)latency.timeScale;
135  }
136  }
137  return 0;
138 }
139 
140 void DisplayLinkManager::OnDisplayLink(CVDisplayLinkRef display_link,
141  const CVTimeStamp* in_now,
142  const CVTimeStamp* in_output_time,
143  CVOptionFlags flags_in,
144  CVOptionFlags* flags_out) {
145  CVTimeStamp inNow = *in_now;
146  CVTimeStamp inOutputTime = *in_output_time;
147  [FlutterRunLoop.mainRunLoop performBlock:^{
148  std::vector<_FlutterDisplayLink*> clients;
149  for (ScreenEntry& entry : entries_) {
150  if (entry.display_link == display_link) {
151  clients = entry.clients;
152  break;
153  }
154  }
155 
156  CFTimeInterval timestamp = (CFTimeInterval)inNow.hostTime / CVGetHostClockFrequency();
157  CFTimeInterval target_timestamp =
158  (CFTimeInterval)inOutputTime.hostTime / CVGetHostClockFrequency();
159 
160  for (_FlutterDisplayLink* client : clients) {
161  [client didFireWithTimestamp:timestamp targetTimestamp:target_timestamp];
162  }
163  }];
164 }
165 } // namespace
166 
167 @interface _FlutterDisplayLinkView : NSView {
168 }
169 
170 @end
171 
173  @"FlutterDisplayLinkViewDidMoveToWindow";
174 
175 @implementation _FlutterDisplayLinkView
176 
177 - (void)viewDidMoveToWindow {
178  [super viewDidMoveToWindow];
179  [[NSNotificationCenter defaultCenter] postNotificationName:kFlutterDisplayLinkViewDidMoveToWindow
180  object:self];
181 }
182 
183 @end
184 
185 @implementation _FlutterDisplayLink
186 
187 @synthesize delegate = _delegate;
188 
189 - (instancetype)initWithView:(NSView*)view {
190  FML_DCHECK(NSThread.isMainThread);
191  if (self = [super init]) {
192  self->_view = [[_FlutterDisplayLinkView alloc] initWithFrame:CGRectZero];
193  [view addSubview:self->_view];
194  _paused = YES;
195  [[NSNotificationCenter defaultCenter] addObserver:self
196  selector:@selector(viewDidChangeWindow:)
197  name:kFlutterDisplayLinkViewDidMoveToWindow
198  object:self->_view];
199  [[NSNotificationCenter defaultCenter] addObserver:self
200  selector:@selector(windowDidChangeScreen:)
201  name:NSWindowDidChangeScreenNotification
202  object:nil];
203  [self updateScreen];
204  }
205  return self;
206 }
207 
208 - (void)invalidate {
209  FML_DCHECK(NSThread.isMainThread);
210  // Unregister observer before removing the view to ensure
211  // that the viewDidChangeWindow notification is not received
212  // while in @synchronized block.
213  [[NSNotificationCenter defaultCenter] removeObserver:self];
214  [_view removeFromSuperview];
215  _view = nil;
216  _delegate = nil;
217  DisplayLinkManager::Instance().UnregisterDisplayLink(self);
218 }
219 
220 - (void)updateScreen {
221  FML_DCHECK(NSThread.isMainThread);
222  DisplayLinkManager::Instance().UnregisterDisplayLink(self);
223  std::optional<CGDirectDisplayID> displayId;
224  NSScreen* screen = _view.window.screen;
225  if (screen != nil) {
226  // https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc
227  _display_id = (CGDirectDisplayID)[
228  [[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
229  } else {
230  _display_id = std::nullopt;
231  }
232  displayId = _display_id;
233 
234  if (displayId.has_value()) {
235  DisplayLinkManager::Instance().RegisterDisplayLink(self, *displayId);
236  }
237 }
238 
239 - (void)viewDidChangeWindow:(NSNotification*)notification {
240  FML_DCHECK(NSThread.isMainThread);
241  NSView* view = notification.object;
242  if (_view == view) {
243  [self updateScreen];
244  }
245 }
246 
247 - (void)windowDidChangeScreen:(NSNotification*)notification {
248  FML_DCHECK(NSThread.isMainThread);
249  NSWindow* window = notification.object;
250  if (_view.window == window) {
251  [self updateScreen];
252  }
253 }
254 
255 - (void)didFireWithTimestamp:(CFTimeInterval)timestamp
256  targetTimestamp:(CFTimeInterval)targetTimestamp {
257  FML_DCHECK(NSThread.isMainThread);
258  if (!_paused) {
259  id<FlutterDisplayLinkDelegate> delegate = _delegate;
260  [delegate onDisplayLink:timestamp targetTimestamp:targetTimestamp];
261  }
262 }
263 
264 - (BOOL)paused {
265  FML_DCHECK(NSThread.isMainThread);
266  return _paused;
267 }
268 
269 - (void)setPaused:(BOOL)paused {
270  FML_DCHECK(NSThread.isMainThread);
271  if (_paused == paused) {
272  return;
273  }
274  _paused = paused;
275  DisplayLinkManager::Instance().PausedDidChange(self);
276 }
277 
278 - (CFTimeInterval)nominalOutputRefreshPeriod {
279  FML_DCHECK(NSThread.isMainThread);
280  CGDirectDisplayID display_id;
281  if (_display_id.has_value()) {
282  display_id = *_display_id;
283  } else {
284  return 0;
285  }
286  return DisplayLinkManager::Instance().GetNominalOutputPeriod(display_id);
287 }
288 
289 @end
290 
291 @implementation FlutterDisplayLink
292 + (instancetype)displayLinkWithView:(NSView*)view {
293  return [[_FlutterDisplayLink alloc] initWithView:view];
294 }
295 
296 - (void)invalidate {
297  [self doesNotRecognizeSelector:_cmd];
298 }
299 
300 @end