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