Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit e06c2c6

Browse files
committed
Add FlutterVSyncWaiterTest
1 parent 36c39b8 commit e06c2c6

File tree

7 files changed

+248
-69
lines changed

7 files changed

+248
-69
lines changed

shell/platform/darwin/macos/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ executable("flutter_desktop_darwin_unittests") {
191191
"framework/Source/FlutterTextInputPluginTest.mm",
192192
"framework/Source/FlutterTextInputSemanticsObjectTest.mm",
193193
"framework/Source/FlutterThreadSynchronizerTest.mm",
194+
"framework/Source/FlutterVSyncWaiterTest.mm",
194195
"framework/Source/FlutterViewControllerTest.mm",
195196
"framework/Source/FlutterViewControllerTestUtils.h",
196197
"framework/Source/FlutterViewControllerTestUtils.mm",

shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#import <AppKit/AppKit.h>
22

3+
@protocol FlutterDisplayLinkDelegate <NSObject>
4+
- (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp;
5+
@end
6+
37
/// Provides notifications of display refresh.
48
///
59
/// Internally FlutterDisplayLink will use at most one CVDisplayLink per
@@ -11,10 +15,11 @@
1115
/// will track view display changes transparently to synchronize
1216
/// update with display refresh.
1317
/// This function must be called on the main thread.
14-
/// |block| will be called from display link thread.
15-
- (instancetype)initWithView:(NSView*)view
16-
block:(void (^)(CFTimeInterval timestamp,
17-
CFTimeInterval targetTimestamp))block;
18+
+ (instancetype)displayLinkWithView:(NSView*)view;
19+
20+
/// Delegate must be set on main thread. Delegate method will be called on
21+
/// on display link thread.
22+
@property(nonatomic, weak) id<FlutterDisplayLinkDelegate> delegate;
1823

1924
/// Pauses and resumes the display link. May be called from any thread.
2025
@property(readwrite) BOOL paused;

shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.mm

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99

1010
@class _FlutterDisplayLinkView;
1111

12-
@interface FlutterDisplayLink () {
12+
@interface _FlutterDisplayLink : FlutterDisplayLink {
1313
_FlutterDisplayLinkView* _view;
1414
std::optional<CGDirectDisplayID> _display_id;
15-
void (^_block)(CFTimeInterval timestamp, CFTimeInterval targetTimestamp);
1615
BOOL _paused;
1716
}
1817

@@ -29,9 +28,9 @@ - (void)didFireWithTimestamp:(CFTimeInterval)timestamp
2928
return instance;
3029
}
3130

32-
void UnregisterDisplayLink(FlutterDisplayLink* display_link);
33-
void RegisterDisplayLink(FlutterDisplayLink* display_link, CGDirectDisplayID display_id);
34-
void PausedDidChange(FlutterDisplayLink* display_link);
31+
void UnregisterDisplayLink(_FlutterDisplayLink* display_link);
32+
void RegisterDisplayLink(_FlutterDisplayLink* display_link, CGDirectDisplayID display_id);
33+
void PausedDidChange(_FlutterDisplayLink* display_link);
3534
CFTimeInterval GetNominalOutputPeriod(CGDirectDisplayID display_id);
3635

3736
private:
@@ -43,7 +42,7 @@ void OnDisplayLink(CVDisplayLinkRef displayLink,
4342

4443
struct ScreenEntry {
4544
CGDirectDisplayID display_id;
46-
std::vector<FlutterDisplayLink*> clients;
45+
std::vector<_FlutterDisplayLink*> clients;
4746

4847
/// Display link for this screen. It is not safe to call display link methods
4948
/// on this object while holding the mutex. Instead the instance should be
@@ -68,7 +67,7 @@ void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running)
6867
}
6968
}
7069

71-
void DisplayLinkManager::UnregisterDisplayLink(FlutterDisplayLink* displayLink) {
70+
void DisplayLinkManager::UnregisterDisplayLink(_FlutterDisplayLink* displayLink) {
7271
std::unique_lock<std::mutex> lock(mutex_);
7372
for (auto entry = entries_.begin(); entry != entries_.end(); ++entry) {
7473
auto it = std::find(entry->clients.begin(), entry->clients.end(), displayLink);
@@ -95,7 +94,7 @@ void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running)
9594
}
9695
}
9796

98-
void DisplayLinkManager::RegisterDisplayLink(FlutterDisplayLink* displayLink,
97+
void DisplayLinkManager::RegisterDisplayLink(_FlutterDisplayLink* displayLink,
9998
CGDirectDisplayID display_id) {
10099
std::unique_lock<std::mutex> lock(mutex_);
101100
for (ScreenEntry& entry : entries_) {
@@ -129,7 +128,7 @@ void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running)
129128
entries_.push_back(entry);
130129
}
131130

132-
void DisplayLinkManager::PausedDidChange(FlutterDisplayLink* displayLink) {
131+
void DisplayLinkManager::PausedDidChange(_FlutterDisplayLink* displayLink) {
133132
std::unique_lock<std::mutex> lock(mutex_);
134133
for (ScreenEntry& entry : entries_) {
135134
auto it = std::find(entry.clients.begin(), entry.clients.end(), displayLink);
@@ -163,7 +162,7 @@ void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running)
163162
const CVTimeStamp* inOutputTime,
164163
CVOptionFlags flagsIn,
165164
CVOptionFlags* flagsOut) {
166-
std::vector<FlutterDisplayLink*> clients;
165+
std::vector<_FlutterDisplayLink*> clients;
167166
{
168167
std::lock_guard<std::mutex> lock(mutex_);
169168
for (ScreenEntry& entry : entries_) {
@@ -179,7 +178,7 @@ void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running)
179178
CFTimeInterval targetTimestamp =
180179
(CFTimeInterval)inOutputTime->hostTime / (CFTimeInterval)inOutputTime->videoTimeScale;
181180

182-
for (FlutterDisplayLink* client : clients) {
181+
for (_FlutterDisplayLink* client : clients) {
183182
[client didFireWithTimestamp:timestamp targetTimestamp:targetTimestamp];
184183
}
185184
}
@@ -203,15 +202,15 @@ - (void)viewDidMoveToWindow {
203202

204203
@end
205204

206-
@implementation FlutterDisplayLink
207-
- (instancetype)initWithView:(NSView*)view
208-
block:(void (^__strong)(CFTimeInterval timestamp,
209-
CFTimeInterval targetTimestamp))block {
205+
@implementation _FlutterDisplayLink
206+
207+
@synthesize delegate = _delegate;
208+
209+
- (instancetype)initWithView:(NSView*)view {
210210
FML_DCHECK([NSThread isMainThread]);
211211
if (self = [super init]) {
212212
self->_view = [[_FlutterDisplayLinkView alloc] initWithFrame:CGRectZero];
213213
[view addSubview:self->_view];
214-
self->_block = block;
215214
_paused = YES;
216215
[[NSNotificationCenter defaultCenter] addObserver:self
217216
selector:@selector(viewDidChangeWindow:)
@@ -232,7 +231,7 @@ - (void)invalidate {
232231
[[NSNotificationCenter defaultCenter] removeObserver:self];
233232
_view = nil;
234233
[self updateScreen];
235-
_block = nil;
234+
_delegate = nil;
236235
}
237236
}
238237

@@ -272,8 +271,8 @@ - (void)windowDidChangeScreen:(NSNotification*)notification {
272271
- (void)didFireWithTimestamp:(CFTimeInterval)timestamp
273272
targetTimestamp:(CFTimeInterval)targetTimestamp {
274273
@synchronized(self) {
275-
if (_block != nil && !_paused) {
276-
_block(timestamp, targetTimestamp);
274+
if (!_paused) {
275+
[_delegate onDisplayLink:timestamp targetTimestamp:targetTimestamp];
277276
}
278277
}
279278
}
@@ -307,3 +306,14 @@ - (CFTimeInterval)nominalOutputRefreshPeriod {
307306
}
308307

309308
@end
309+
310+
@implementation FlutterDisplayLink
311+
+ (instancetype)displayLinkWithView:(NSView*)view {
312+
return [[_FlutterDisplayLink alloc] initWithView:view];
313+
}
314+
315+
- (void)invalidate {
316+
[self doesNotRecognizeSelector:_cmd];
317+
}
318+
319+
@end

shell/platform/darwin/macos/framework/Source/FlutterEngine.mm

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h"
1919
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h"
2020
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
21+
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
2122
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMenuPlugin.h"
2223
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h"
2324
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h"
@@ -737,16 +738,17 @@ - (void)registerViewController:(FlutterViewController*)controller forId:(Flutter
737738
- (void)viewControllerViewDidLoad:(FlutterViewController*)viewController {
738739
__weak FlutterEngine* weakSelf = self;
739740
FlutterVSyncWaiter* waiter = [[FlutterVSyncWaiter alloc]
740-
initWithView:viewController.view
741-
block:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp, uintptr_t baton) {
742-
// CAMediaTime and flutter time are both mach_absolute_time.
743-
uint64_t timeNanos = timestamp * 1000000000;
744-
uint64_t targetTimeNanos = targetTimestamp * 1000000000;
745-
FlutterEngine* engine = weakSelf;
746-
if (engine) {
747-
engine->_embedderAPI.OnVsync(_engine, baton, timeNanos, targetTimeNanos);
748-
}
749-
}];
741+
initWithDisplayLink:[FlutterDisplayLink displayLinkWithView:viewController.view]
742+
block:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp,
743+
uintptr_t baton) {
744+
// CAMediaTime and flutter time are both mach_absolute_time.
745+
uint64_t timeNanos = timestamp * 1000000000;
746+
uint64_t targetTimeNanos = targetTimestamp * 1000000000;
747+
FlutterEngine* engine = weakSelf;
748+
if (engine) {
749+
engine->_embedderAPI.OnVsync(_engine, baton, timeNanos, targetTimeNanos);
750+
}
751+
}];
750752
FML_DCHECK([_vsyncWaiters objectForKey:@(viewController.viewId)] == nil);
751753
@synchronized(_vsyncWaiters) {
752754
[_vsyncWaiters setObject:waiter forKey:@(viewController.viewId)];

shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
#import <AppKit/AppKit.h>
22

3+
@class FlutterDisplayLink;
4+
35
@interface FlutterVSyncWaiter : NSObject
46

57
/// Creates new waiter instance tied to provided NSView.
68
/// This function must be called on the main thread.
79
///
810
/// Provided |block| will be invoked on same thread as -waitForVSync:.
9-
- (instancetype)initWithView:(NSView*)view
10-
block:(void (^)(CFTimeInterval timestamp,
11-
CFTimeInterval targetTimestamp,
12-
uintptr_t baton))block;
11+
- (instancetype)initWithDisplayLink:(FlutterDisplayLink*)displayLink
12+
block:(void (^)(CFTimeInterval timestamp,
13+
CFTimeInterval targetTimestamp,
14+
uintptr_t baton))block;
1315

1416
/// Schedules |baton| to be signaled on next display refresh.
1517
/// The block provided in the initializer will be invoked on same thread

shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
} while (0)
2727
#endif
2828

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.
34+
static const CFTimeInterval kTimerLatencyCompensation = 0.001;
35+
2936
@implementation FlutterVSyncWaiter {
3037
std::optional<std::uintptr_t> _pending_baton;
3138
FlutterDisplayLink* _displayLink;
@@ -34,43 +41,37 @@ @implementation FlutterVSyncWaiter {
3441
CFTimeInterval _lastTargetTimestamp;
3542
}
3643

37-
- (instancetype)initWithView:(NSView*)view
38-
block:(void (^)(CFTimeInterval, CFTimeInterval, uintptr_t))block {
44+
- (instancetype)initWithDisplayLink:(FlutterDisplayLink*)displayLink
45+
block:(void (^)(CFTimeInterval timestamp,
46+
CFTimeInterval targetTimestamp,
47+
uintptr_t baton))block {
3948
FML_DCHECK([NSThread isMainThread]);
4049
if (self = [super init]) {
4150
_block = block;
4251

43-
__weak FlutterVSyncWaiter* weakSelf = self;
44-
_displayLink = [[FlutterDisplayLink alloc]
45-
initWithView:view
46-
block:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp) {
47-
[weakSelf _onDisplayLinkTimestamp:timestamp targetTimestamp:targetTimestamp];
48-
}];
52+
_displayLink = displayLink;
53+
_displayLink.delegate = self;
4954
// Get at least one callback to initialize _lastTargetTimestamp.
5055
_displayLink.paused = NO;
5156
}
5257
return self;
5358
}
5459

55-
- (void)_onDisplayLinkTimestamp:(CFTimeInterval)timestamp
56-
targetTimestamp:(CFTimeInterval)targetTimestamp {
60+
- (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp {
5761
_lastTargetTimestamp = targetTimestamp;
5862

63+
if (!_pending_baton.has_value()) {
64+
TRACE_VSYNC("DisplayLinkPaused", size_t(0));
65+
_displayLink.paused = YES;
66+
return;
67+
}
68+
5969
// CVDisplayLink callback is called one and a half frame before the target
6070
// timestamp. That can cause frame-pacing issues if the frame is rendered too early,
6171
// it may also trigger frame start before events are processed.
62-
//
63-
// 1ms is substracted from target time to compensate for timer scheduling latency.
64-
// At that point events should be processed and there is additional scheduling
65-
// logic in [FlutterSurfaceManager] to ensure that final frame is not presented
66-
// too early.
67-
CFTimeInterval minStart = targetTimestamp - _displayLink.nominalOutputRefreshPeriod - 0.001;
72+
CFTimeInterval minStart = targetTimestamp - _displayLink.nominalOutputRefreshPeriod;
6873
CFTimeInterval current = CACurrentMediaTime();
69-
CFTimeInterval remaining = minStart - current;
70-
71-
if (remaining < 0) {
72-
remaining = 0;
73-
}
74+
CFTimeInterval remaining = std::max(minStart - current - kTimerLatencyCompensation, 0.0);
7475

7576
TRACE_VSYNC("DisplayLinkCallback-Original", _pending_baton.value_or(0));
7677

@@ -79,11 +80,6 @@ - (void)_onDisplayLinkTimestamp:(CFTimeInterval)timestamp
7980
repeats:NO
8081
block:^(NSTimer* _Nonnull timer) {
8182
TRACE_VSYNC("DisplayLinkCallback-Delayed", _pending_baton.value_or(0));
82-
if (!_pending_baton.has_value()) {
83-
TRACE_VSYNC("DisplayLinkPaused", size_t(0));
84-
_displayLink.paused = YES;
85-
return;
86-
}
8783
_block(minStart, targetTimestamp, *_pending_baton);
8884
_pending_baton = std::nullopt;
8985
}];
@@ -112,24 +108,23 @@ - (void)waitForVSync:(uintptr_t)baton {
112108
// Also use a timer if display link does not belong to any display
113109
// (nominalOutputRefreshPeriod being 0)
114110
CFTimeInterval delay = 0;
111+
CFTimeInterval start = CACurrentMediaTime();
115112
if (tick_interval != 0 && _lastTargetTimestamp != 0) {
116113
CFTimeInterval phase = fmod(_lastTargetTimestamp, tick_interval);
117114
CFTimeInterval now = CACurrentMediaTime();
118-
CFTimeInterval start = now - (fmod(now, tick_interval)) + phase;
115+
start = now - (fmod(now, tick_interval)) + phase;
119116
if (start < now) {
120117
start += tick_interval;
121118
}
122-
delay = start - now;
119+
delay = std::max(start - now - kTimerLatencyCompensation, 0.0);
123120
}
124-
125121
NSTimer* timer = [NSTimer timerWithTimeInterval:delay
126122
repeats:NO
127123
block:^(NSTimer* timer) {
128-
CFTimeInterval timestamp = CACurrentMediaTime();
129124
CFTimeInterval targetTimestamp =
130-
timestamp + tick_interval;
125+
start + tick_interval;
131126
TRACE_VSYNC("SynthesizedInitialVSync", baton);
132-
_block(timestamp, targetTimestamp, baton);
127+
_block(start, targetTimestamp, baton);
133128
}];
134129
[_runLoop addTimer:timer forMode:NSRunLoopCommonModes];
135130
_displayLink.paused = NO;

0 commit comments

Comments
 (0)