Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
FlutterVSyncWaiter.mm
Go to the documentation of this file.
1#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h"
2#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
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
29@interface FlutterVSyncWaiter () <FlutterDisplayLinkDelegate>
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.
34static 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;
55 // Get at least one callback to initialize _lastTargetTimestamp.
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
FlutterDisplayLink * _displayLink
static const CFTimeInterval kTimerLatencyCompensation
NSRunLoop * _runLoop
#define TRACE_VSYNC(event_type, baton)
CFTimeInterval _lastTargetTimestamp
void(^ _block)(CFTimeInterval, CFTimeInterval, uintptr_t)
BOOL _warmUpFrame
#define FML_LOG(severity)
Definition logging.h:82
#define FML_DCHECK(condition)
Definition logging.h:103
CADisplayLink * _displayLink
int BOOL