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

Commit 9423f12

Browse files
committed
Add FlutterVSyncWaiterTest
1 parent 36c39b8 commit 9423f12

File tree

7 files changed

+247
-64
lines changed

7 files changed

+247
-64
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: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
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+
static const CFTimeInterval kTimerLatencyCompensation = 0.001;
34+
2935
@implementation FlutterVSyncWaiter {
3036
std::optional<std::uintptr_t> _pending_baton;
3137
FlutterDisplayLink* _displayLink;
@@ -34,28 +40,31 @@ @implementation FlutterVSyncWaiter {
3440
CFTimeInterval _lastTargetTimestamp;
3541
}
3642

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

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-
}];
51+
_displayLink = displayLink;
52+
_displayLink.delegate = self;
4953
// Get at least one callback to initialize _lastTargetTimestamp.
5054
_displayLink.paused = NO;
5155
}
5256
return self;
5357
}
5458

55-
- (void)_onDisplayLinkTimestamp:(CFTimeInterval)timestamp
56-
targetTimestamp:(CFTimeInterval)targetTimestamp {
59+
- (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp {
5760
_lastTargetTimestamp = targetTimestamp;
5861

62+
if (!_pending_baton.has_value()) {
63+
TRACE_VSYNC("DisplayLinkPaused", size_t(0));
64+
_displayLink.paused = YES;
65+
return;
66+
}
67+
5968
// CVDisplayLink callback is called one and a half frame before the target
6069
// timestamp. That can cause frame-pacing issues if the frame is rendered too early,
6170
// it may also trigger frame start before events are processed.
@@ -64,13 +73,9 @@ - (void)_onDisplayLinkTimestamp:(CFTimeInterval)timestamp
6473
// At that point events should be processed and there is additional scheduling
6574
// logic in [FlutterSurfaceManager] to ensure that final frame is not presented
6675
// too early.
67-
CFTimeInterval minStart = targetTimestamp - _displayLink.nominalOutputRefreshPeriod - 0.001;
76+
CFTimeInterval minStart = targetTimestamp - _displayLink.nominalOutputRefreshPeriod;
6877
CFTimeInterval current = CACurrentMediaTime();
69-
CFTimeInterval remaining = minStart - current;
70-
71-
if (remaining < 0) {
72-
remaining = 0;
73-
}
78+
CFTimeInterval remaining = std::max(minStart - current - kTimerLatencyCompensation, 0.0);
7479

7580
TRACE_VSYNC("DisplayLinkCallback-Original", _pending_baton.value_or(0));
7681

@@ -79,11 +84,6 @@ - (void)_onDisplayLinkTimestamp:(CFTimeInterval)timestamp
7984
repeats:NO
8085
block:^(NSTimer* _Nonnull timer) {
8186
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-
}
8787
_block(minStart, targetTimestamp, *_pending_baton);
8888
_pending_baton = std::nullopt;
8989
}];
@@ -112,24 +112,23 @@ - (void)waitForVSync:(uintptr_t)baton {
112112
// Also use a timer if display link does not belong to any display
113113
// (nominalOutputRefreshPeriod being 0)
114114
CFTimeInterval delay = 0;
115+
CFTimeInterval start = CACurrentMediaTime();
115116
if (tick_interval != 0 && _lastTargetTimestamp != 0) {
116117
CFTimeInterval phase = fmod(_lastTargetTimestamp, tick_interval);
117118
CFTimeInterval now = CACurrentMediaTime();
118-
CFTimeInterval start = now - (fmod(now, tick_interval)) + phase;
119+
start = now - (fmod(now, tick_interval)) + phase;
119120
if (start < now) {
120121
start += tick_interval;
121122
}
122-
delay = start - now;
123+
delay = std::max(start - now - kTimerLatencyCompensation, 0.0);
123124
}
124-
125125
NSTimer* timer = [NSTimer timerWithTimeInterval:delay
126126
repeats:NO
127127
block:^(NSTimer* timer) {
128-
CFTimeInterval timestamp = CACurrentMediaTime();
129128
CFTimeInterval targetTimestamp =
130-
timestamp + tick_interval;
129+
start + tick_interval;
131130
TRACE_VSYNC("SynthesizedInitialVSync", baton);
132-
_block(timestamp, targetTimestamp, baton);
131+
_block(start, targetTimestamp, baton);
133132
}];
134133
[_runLoop addTimer:timer forMode:NSRunLoopCommonModes];
135134
_displayLink.paused = NO;

0 commit comments

Comments
 (0)