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