Flutter macOS Embedder
FlutterVSyncWaiter.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 <optional>
7 #include <vector>
8 
9 #include "flutter/fml/logging.h"
10 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
11 #import "flutter/shell/platform/darwin/macos/InternalFlutterSwift/InternalFlutterSwift.h"
13 
14 #if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_PROFILE)
15 #define VSYNC_TRACING_ENABLED 1
16 #endif
17 
18 #if VSYNC_TRACING_ENABLED
19 #include <OSLog/OSLog.h>
20 
21 // Trace vsync events using os_signpost so that they can be seen in Instruments "Points of
22 // Interest".
23 #define TRACE_VSYNC(event_type, baton) \
24  do { \
25  os_log_t log = os_log_create("FlutterVSync", "PointsOfInterest"); \
26  os_signpost_event_emit(log, OS_SIGNPOST_ID_EXCLUSIVE, event_type, "baton %lx", baton); \
27  } while (0)
28 #else
29 #define TRACE_VSYNC(event_type, baton) \
30  do { \
31  } while (0)
32 #endif
33 
35 @end
36 
37 // It's preferable to fire the timers slightly early than too late due to scheduling latency.
38 // 1ms before vsync should be late enough for all events to be processed.
39 static const CFTimeInterval kTimerLatencyCompensation = 0.001;
40 
41 @implementation FlutterVSyncWaiter {
42  std::optional<std::uintptr_t> _pendingBaton;
44  void (^_block)(CFTimeInterval, CFTimeInterval, uintptr_t);
45  CFTimeInterval _lastTargetTimestamp;
47 }
48 
49 - (instancetype)initWithDisplayLink:(FlutterDisplayLink*)displayLink
50  block:(void (^)(CFTimeInterval timestamp,
51  CFTimeInterval targetTimestamp,
52  uintptr_t baton))block {
53  FML_DCHECK([NSThread isMainThread]);
54  if (self = [super init]) {
55  _block = block;
56 
57  _displayLink = displayLink;
58  _displayLink.delegate = self;
59  // Get at least one callback to initialize _lastTargetTimestamp.
60  _displayLink.paused = NO;
61  _warmUpFrame = YES;
62  }
63  return self;
64 }
65 
66 // Called from display link thread.
67 - (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp {
68  if (_lastTargetTimestamp == 0) {
69  // Initial vsync - timestamp will be used to determine vsync phase.
70  _lastTargetTimestamp = targetTimestamp;
71  _displayLink.paused = YES;
72  } else {
73  _lastTargetTimestamp = targetTimestamp;
74 
75  // CVDisplayLink callback is called one and a half frame before the target
76  // timestamp. That can cause frame-pacing issues if the frame is rendered too early,
77  // it may also trigger frame start before events are processed.
78  CFTimeInterval minStart = targetTimestamp - _displayLink.nominalOutputRefreshPeriod;
79  CFTimeInterval current = CACurrentMediaTime();
80  CFTimeInterval remaining = std::max(minStart - current - kTimerLatencyCompensation, 0.0);
81 
82  TRACE_VSYNC("DisplayLinkCallback-Original", _pendingBaton.value_or(0));
83 
84  [FlutterRunLoop.mainRunLoop
85  performAfterDelay:remaining
86  block:^{
87  if (!_pendingBaton.has_value()) {
88  TRACE_VSYNC("DisplayLinkPaused", size_t(0));
89  _displayLink.paused = YES;
90  return;
91  }
92  TRACE_VSYNC("DisplayLinkCallback-Delayed", _pendingBaton.value_or(0));
93  _block(minStart, targetTimestamp, *_pendingBaton);
94  _pendingBaton = std::nullopt;
95  }];
96  }
97 }
98 
99 // Called from UI thread.
100 - (void)waitForVSync:(uintptr_t)baton {
101  FML_DCHECK([NSThread isMainThread]);
102  // CVDisplayLink start -> callback latency is two frames, there is
103  // no need to delay the warm-up frame.
104  if (_warmUpFrame) {
105  _warmUpFrame = NO;
106  TRACE_VSYNC("WarmUpFrame", baton);
107  CFTimeInterval now = CACurrentMediaTime();
108  _block(now, now, baton);
109  return;
110  }
111 
112  if (_pendingBaton.has_value()) {
113  [FlutterLogger logWarning:@"Engine requested vsync while another was pending"];
114  _block(0, 0, *_pendingBaton);
115  _pendingBaton = std::nullopt;
116  }
117 
118  TRACE_VSYNC("VSyncRequest", _pendingBaton.value_or(0));
119 
120  CFTimeInterval tick_interval = _displayLink.nominalOutputRefreshPeriod;
121  if (_displayLink.paused || tick_interval == 0 || _lastTargetTimestamp == 0) {
122  // When starting display link the first notification will come in the middle
123  // of next frame, which would incur a whole frame period of latency.
124  // To avoid that, first vsync notification will be fired using a timer
125  // scheduled to fire where the next frame is expected to start.
126  // Also use a timer if display link does not belong to any display
127  // (nominalOutputRefreshPeriod being 0)
128 
129  // Start of the vsync interval.
130  CFTimeInterval start = CACurrentMediaTime();
131 
132  // Timer delay is calculated as the time to the next frame start.
133  CFTimeInterval delay = 0;
134 
135  if (tick_interval != 0 && _lastTargetTimestamp != 0) {
136  CFTimeInterval phase = fmod(_lastTargetTimestamp, tick_interval);
137  CFTimeInterval now = start;
138  start = now - (fmod(now, tick_interval)) + phase;
139  if (start < now) {
140  start += tick_interval;
141  }
142  delay = std::max(start - now - kTimerLatencyCompensation, 0.0);
143  }
144 
145  [FlutterRunLoop.mainRunLoop performAfterDelay:delay
146  block:^{
147  CFTimeInterval targetTime = start + tick_interval;
148  TRACE_VSYNC("SynthesizedInitialVSync", baton);
149  _block(start, targetTime, baton);
150  }];
151  _displayLink.paused = NO;
152  } else {
153  _pendingBaton = baton;
154  }
155 }
156 
157 - (void)invalidate {
158  // It is possible that there is pending vsync request while the view for which
159  // this waiter belongs is being destroyed. In that case trigger the vsync
160  // immediately to avoid deadlock.
161  if (_pendingBaton.has_value()) {
162  CFTimeInterval now = CACurrentMediaTime();
163  _block(now, now, _pendingBaton.value());
164  _pendingBaton = std::nullopt;
165  }
166 
167  [_displayLink invalidate];
168  _displayLink = nil;
169 }
170 
171 - (void)dealloc {
172  FML_DCHECK(_displayLink == nil);
173 }
174 
175 @end
FlutterDisplayLink * _displayLink
static const CFTimeInterval kTimerLatencyCompensation
#define TRACE_VSYNC(event_type, baton)
CFTimeInterval _lastTargetTimestamp
void(^ _block)(CFTimeInterval, CFTimeInterval, uintptr_t)
BOOL _warmUpFrame