| Flutter macOS Embedder |

FlutterDisplayLink.mm
Go to the documentation of this file.
1 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
2 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h"
3
4 #include "flutter/fml/logging.h"
5
6 #include <algorithm>
7 #include <mutex>
8 #include <optional>
9 #include <thread>
10 #include <vector>
11
12 @class _FlutterDisplayLinkView;
13
14 @interface _FlutterDisplayLink : FlutterDisplayLink {
15 _FlutterDisplayLinkView* _view;
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
169 static NSString* const kFlutterDisplayLinkViewDidMoveToWindow =
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
FlutterDisplayLink.h
kFlutterDisplayLinkViewDidMoveToWindow
static NSString *const kFlutterDisplayLinkViewDidMoveToWindow
Definition: FlutterDisplayLink.mm:169
FlutterRunLoop.h
_FlutterDisplayLink
Definition: FlutterDisplayLink.mm:14
_FlutterDisplayLink::_display_id
std::optional< CGDirectDisplayID > _display_id
Definition: FlutterDisplayLink.mm:16
_FlutterDisplayLink::_view
_FlutterDisplayLinkView * _view
Definition: FlutterDisplayLink.mm:15
_FlutterDisplayLink::_paused
BOOL _paused
Definition: FlutterDisplayLink.mm:17
_FlutterDisplayLinkView
Definition: FlutterDisplayLink.mm:164
FlutterDisplayLink
Definition: FlutterDisplayLink.h:19
-[FlutterDisplayLink invalidate]
void invalidate()
Invalidates the display link.
Definition: FlutterDisplayLink.mm:293
FlutterDisplayLink::nominalOutputRefreshPeriod
CFTimeInterval nominalOutputRefreshPeriod
Definition: FlutterDisplayLink.h:35
FlutterDisplayLink::paused
BOOL paused
Pauses and resumes the display link.
Definition: FlutterDisplayLink.h:30
FlutterDisplayLink::delegate
id< FlutterDisplayLinkDelegate > delegate
Definition: FlutterDisplayLink.h:27
FlutterRunLoop
Definition: FlutterRunLoop.h:16
-[FlutterRunLoop performBlock:]
void performBlock:(void(^ block)(void))
Definition: FlutterRunLoop.mm:107