Flutter macOS Embedder
FlutterVSyncWaiter.mm
Go to the documentation of this file.
3 
4 #include "flutter/fml/logging.h"
5 
6 #include <optional>
7 #include <vector>
8 
9 #if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_PROFILE)
10 #define VSYNC_TRACING_ENABLED 1
11 #endif
12 
13 #if VSYNC_TRACING_ENABLED
14 #include <OSLog/OSLog.h>
15 
16 // Trace vsync events using os_signpost so that they can be seen in Instruments "Points of
17 // Interest".
18 #define TRACE_VSYNC(event_type, baton) \
19  do { \
20  os_log_t log = os_log_create("FlutterVSync", "PointsOfInterest"); \
21  os_signpost_event_emit(log, OS_SIGNPOST_ID_EXCLUSIVE, event_type, "baton %lx", baton); \
22  } while (0)
23 #else
24 #define TRACE_VSYNC(event_type, baton) \
25  do { \
26  } while (0)
27 #endif
28 
30 @end
31 
32 // It's preferable to fire the timers slightly early than too late due to scheduling latency.
33 // 1ms before vsync should be late enough for all events to be processed.
34 static const CFTimeInterval kTimerLatencyCompensation = 0.001;
35 
36 @implementation FlutterVSyncWaiter {
37  std::optional<std::uintptr_t> _pendingBaton;
39  void (^_block)(CFTimeInterval, CFTimeInterval, uintptr_t);
40  NSRunLoop* _runLoop;
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 on same thread as the vsync request (UI thread).
63 - (void)processDisplayLink:(CFTimeInterval)timestamp
64  targetTimestamp:(CFTimeInterval)targetTimestamp {
65  FML_DCHECK([NSRunLoop currentRunLoop] == _runLoop);
66 
67  _lastTargetTimestamp = targetTimestamp;
68 
69  // CVDisplayLink callback is called one and a half frame before the target
70  // timestamp. That can cause frame-pacing issues if the frame is rendered too early,
71  // it may also trigger frame start before events are processed.
72  CFTimeInterval minStart = targetTimestamp - _displayLink.nominalOutputRefreshPeriod;
73  CFTimeInterval current = CACurrentMediaTime();
74  CFTimeInterval remaining = std::max(minStart - current - kTimerLatencyCompensation, 0.0);
75 
76  TRACE_VSYNC("DisplayLinkCallback-Original", _pendingBaton.value_or(0));
77 
78  NSTimer* timer = [NSTimer
79  timerWithTimeInterval:remaining
80  repeats:NO
81  block:^(NSTimer* _Nonnull timer) {
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  [_runLoop addTimer:timer forMode:NSRunLoopCommonModes];
92 }
93 
94 // Called from display link thread.
95 - (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp {
96  @synchronized(self) {
97  if (_runLoop == nil) {
98  // Initial vsync - timestamp will be used to determine vsync phase.
99  _lastTargetTimestamp = targetTimestamp;
100  _displayLink.paused = YES;
101  } else {
102  [_runLoop performBlock:^{
103  [self processDisplayLink:timestamp targetTimestamp:targetTimestamp];
104  }];
105  }
106  }
107 }
108 
109 // Called from UI thread.
110 - (void)waitForVSync:(uintptr_t)baton {
111  // CVDisplayLink start -> callback latency is two frames, there is
112  // no need to delay the warm-up frame.
113  if (_warmUpFrame) {
114  _warmUpFrame = NO;
115  TRACE_VSYNC("WarmUpFrame", baton);
116  [[NSRunLoop currentRunLoop] performBlock:^{
117  CFTimeInterval now = CACurrentMediaTime();
118  _block(now, now, baton);
119  }];
120  return;
121  }
122 
123  // RunLoop is accessed both from main thread and from the display link thread.
124  @synchronized(self) {
125  if (_runLoop == nil) {
126  _runLoop = [NSRunLoop currentRunLoop];
127  }
128  }
129 
130  FML_DCHECK(_runLoop == [NSRunLoop currentRunLoop]);
131  if (_pendingBaton.has_value()) {
132  FML_LOG(WARNING) << "Engine requested vsync while another was pending";
133  _block(0, 0, *_pendingBaton);
134  _pendingBaton = std::nullopt;
135  }
136 
137  TRACE_VSYNC("VSyncRequest", _pendingBaton.value_or(0));
138 
139  CFTimeInterval tick_interval = _displayLink.nominalOutputRefreshPeriod;
140  if (_displayLink.paused || tick_interval == 0) {
141  // When starting display link the first notification will come in the middle
142  // of next frame, which would incur a whole frame period of latency.
143  // To avoid that, first vsync notification will be fired using a timer
144  // scheduled to fire where the next frame is expected to start.
145  // Also use a timer if display link does not belong to any display
146  // (nominalOutputRefreshPeriod being 0)
147 
148  // Start of the vsync interval.
149  CFTimeInterval start = CACurrentMediaTime();
150 
151  // Timer delay is calculated as the time to the next frame start.
152  CFTimeInterval delay = 0;
153 
154  if (tick_interval != 0 && _lastTargetTimestamp != 0) {
155  CFTimeInterval phase = fmod(_lastTargetTimestamp, tick_interval);
156  CFTimeInterval now = start;
157  start = now - (fmod(now, tick_interval)) + phase;
158  if (start < now) {
159  start += tick_interval;
160  }
161  delay = std::max(start - now - kTimerLatencyCompensation, 0.0);
162  }
163 
164  NSTimer* timer = [NSTimer timerWithTimeInterval:delay
165  repeats:NO
166  block:^(NSTimer* timer) {
167  CFTimeInterval targetTimestamp =
168  start + tick_interval;
169  TRACE_VSYNC("SynthesizedInitialVSync", baton);
170  _block(start, targetTimestamp, baton);
171  }];
172  [_runLoop addTimer:timer forMode:NSRunLoopCommonModes];
173  _displayLink.paused = NO;
174  } else {
175  _pendingBaton = baton;
176  }
177 }
178 
179 - (void)dealloc {
180  if (_pendingBaton.has_value()) {
181  FML_LOG(WARNING) << "Deallocating FlutterVSyncWaiter with a pending vsync";
182  }
183  [_displayLink invalidate];
184 }
185 
186 @end
_warmUpFrame
BOOL _warmUpFrame
Definition: FlutterVSyncWaiter.mm:42
_runLoop
NSRunLoop * _runLoop
Definition: FlutterVSyncWaiter.mm:40
FlutterVSyncWaiter.h
kTimerLatencyCompensation
static const CFTimeInterval kTimerLatencyCompensation
Definition: FlutterVSyncWaiter.mm:34
FlutterVSyncWaiter
Definition: FlutterVSyncWaiter.h:8
TRACE_VSYNC
#define TRACE_VSYNC(event_type, baton)
Definition: FlutterVSyncWaiter.mm:18
_displayLink
FlutterDisplayLink * _displayLink
Definition: FlutterVSyncWaiter.mm:36
_lastTargetTimestamp
CFTimeInterval _lastTargetTimestamp
Definition: FlutterVSyncWaiter.mm:41
_block
void(^ _block)(CFTimeInterval, CFTimeInterval, uintptr_t)