[macOS] Use CVDisplayLink to drive repaint (flutter/engine#49159)

Fixes https://github.com/flutter/flutter/issues/49757

This PR synchronises updates with display refresh allowing for true
120hz repaint. It also enforces frame pacing resulting in smoother
experience at both 60hz and 120hz.

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
Matej Knopp 2024-02-27 20:48:26 +01:00 committed by GitHub
parent 75bfeb8fb0
commit 8ff01af723
23 changed files with 1112 additions and 36 deletions

View File

@ -29525,6 +29525,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterCom
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLinkTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderExternalTextureTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm + ../../../flutter/LICENSE
@ -29575,6 +29576,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThr
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizerTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterUmbrellaImportTests.m + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiterTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm + ../../../flutter/LICENSE
@ -32386,6 +32388,9 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterCompo
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLinkTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderExternalTextureTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm
@ -32436,6 +32441,9 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThrea
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizerTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterUmbrellaImportTests.m
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiterTest.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm
FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm

View File

@ -66,6 +66,8 @@ source_set("flutter_framework_source") {
"framework/Source/FlutterCompositor.mm",
"framework/Source/FlutterDartProject.mm",
"framework/Source/FlutterDartProject_Internal.h",
"framework/Source/FlutterDisplayLink.h",
"framework/Source/FlutterDisplayLink.mm",
"framework/Source/FlutterEmbedderKeyResponder.h",
"framework/Source/FlutterEmbedderKeyResponder.mm",
"framework/Source/FlutterEngine.mm",
@ -101,6 +103,8 @@ source_set("flutter_framework_source") {
"framework/Source/FlutterTextureRegistrar.mm",
"framework/Source/FlutterThreadSynchronizer.h",
"framework/Source/FlutterThreadSynchronizer.mm",
"framework/Source/FlutterVSyncWaiter.h",
"framework/Source/FlutterVSyncWaiter.mm",
"framework/Source/FlutterView.h",
"framework/Source/FlutterView.mm",
"framework/Source/FlutterViewController.mm",
@ -173,6 +177,7 @@ executable("flutter_desktop_darwin_unittests") {
"framework/Source/FlutterAppDelegateTest.mm",
"framework/Source/FlutterAppLifecycleDelegateTest.mm",
"framework/Source/FlutterChannelKeyResponderTest.mm",
"framework/Source/FlutterDisplayLinkTest.mm",
"framework/Source/FlutterEmbedderExternalTextureTest.mm",
"framework/Source/FlutterEmbedderKeyResponderTest.mm",
"framework/Source/FlutterEngineTest.mm",
@ -187,6 +192,7 @@ executable("flutter_desktop_darwin_unittests") {
"framework/Source/FlutterTextInputPluginTest.mm",
"framework/Source/FlutterTextInputSemanticsObjectTest.mm",
"framework/Source/FlutterThreadSynchronizerTest.mm",
"framework/Source/FlutterVSyncWaiterTest.mm",
"framework/Source/FlutterViewControllerTest.mm",
"framework/Source/FlutterViewControllerTestUtils.h",
"framework/Source/FlutterViewControllerTestUtils.mm",

View File

@ -69,10 +69,17 @@ bool FlutterCompositor::Present(FlutterViewId view_id,
}
}
[view.surfaceManager present:surfaces
notify:^{
PresentPlatformViews(view, layers, layers_count);
}];
CFTimeInterval presentation_time = 0;
if (layers_count > 0 && layers[0]->presentation_time != 0) {
presentation_time = layers[0]->presentation_time / 1'000'000'000.0;
}
[view.surfaceManager presentSurfaces:surfaces
atTime:presentation_time
notify:^{
PresentPlatformViews(view, layers, layers_count);
}];
return true;
}

View File

@ -0,0 +1,40 @@
#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERDISPLAYLINK_H_
#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERDISPLAYLINK_H_
#import <AppKit/AppKit.h>
@protocol FlutterDisplayLinkDelegate <NSObject>
- (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp;
@end
/// Provides notifications of display refresh.
///
/// Internally FlutterDisplayLink will use at most one CVDisplayLink per
/// screen shared for all views belonging to that screen. This is necessary
/// because each CVDisplayLink comes with its own thread.
@interface FlutterDisplayLink : NSObject
/// Creates new instance tied to provided NSView. FlutterDisplayLink
/// will track view display changes transparently to synchronize
/// update with display refresh.
/// This function must be called on the main thread.
+ (instancetype)displayLinkWithView:(NSView*)view;
/// Delegate must be set on main thread. Delegate method will be called on
/// on display link thread.
@property(nonatomic, weak) id<FlutterDisplayLinkDelegate> delegate;
/// Pauses and resumes the display link. May be called from any thread.
@property(readwrite) BOOL paused;
/// Returns the nominal refresh period of the display to which the view
/// currently belongs (in seconds). If view does not belong to any display,
/// returns 0. Can be called from any thread.
@property(readonly) CFTimeInterval nominalOutputRefreshPeriod;
/// Invalidates the display link. Must be called on the main thread.
- (void)invalidate;
@end
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERDISPLAYLINK_H_

View File

@ -0,0 +1,355 @@
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
#include "flutter/fml/logging.h"
#include <algorithm>
#include <optional>
#include <thread>
#include <vector>
// Note on thread safety and locking:
//
// There are three mutexes used within the scope of this file:
// - CVDisplayLink internal mutex. This is locked during every CVDisplayLink method
// and is also held while display link calls the output handler.
// - DisplayLinkManager mutex.
// - _FlutterDisplayLink mutex (through @synchronized blocks).
//
// Special care must be taken to avoid deadlocks. Because CVDisplayLink holds the
// mutex for the entire duration of the output handler, it is necessary for
// DisplayLinkManager to not call any CVDisplayLink methods while holding its
// mutex. Instead it must retain the display link instance and then call the
// appropriate method with the mutex unlocked.
//
// Similarly _FlutterDisplayLink must not call any DisplayLinkManager methods
// within the @synchronized block.
@class _FlutterDisplayLinkView;
@interface _FlutterDisplayLink : FlutterDisplayLink {
_FlutterDisplayLinkView* _view;
std::optional<CGDirectDisplayID> _display_id;
BOOL _paused;
}
- (void)didFireWithTimestamp:(CFTimeInterval)timestamp
targetTimestamp:(CFTimeInterval)targetTimestamp;
@end
namespace {
class DisplayLinkManager {
public:
static DisplayLinkManager& Instance() {
static DisplayLinkManager instance;
return instance;
}
void UnregisterDisplayLink(_FlutterDisplayLink* display_link);
void RegisterDisplayLink(_FlutterDisplayLink* display_link, CGDirectDisplayID display_id);
void PausedDidChange(_FlutterDisplayLink* display_link);
CFTimeInterval GetNominalOutputPeriod(CGDirectDisplayID display_id);
private:
void OnDisplayLink(CVDisplayLinkRef display_link,
const CVTimeStamp* in_now,
const CVTimeStamp* in_output_time,
CVOptionFlags flags_in,
CVOptionFlags* flags_out);
struct ScreenEntry {
CGDirectDisplayID display_id;
std::vector<_FlutterDisplayLink*> clients;
/// Display link for this screen. It is not safe to call display link methods
/// on this object while holding the mutex. Instead the instance should be
/// retained, mutex unlocked and then released.
CVDisplayLinkRef display_link_locked;
bool ShouldBeRunning() {
return std::any_of(clients.begin(), clients.end(),
[](FlutterDisplayLink* link) { return !link.paused; });
}
};
std::vector<ScreenEntry> entries_;
std::mutex mutex_;
};
void RunOrStopDisplayLink(CVDisplayLinkRef display_link, bool should_be_running) {
bool is_running = CVDisplayLinkIsRunning(display_link);
if (should_be_running && !is_running) {
if (CVDisplayLinkStart(display_link) == kCVReturnError) {
// CVDisplayLinkStart will fail if it was called from the display link thread.
// The problem is that it CVDisplayLinkStop doesn't clean the pthread_t value in the display
// link itself. If the display link is started and stopped before before the UI thread is
// started (*), pthread_self() of the UI thread may have same value as the one stored in
// CVDisplayLink. Because this can happen at most once starting the display link from a
// temporary thread is a reasonable workaround.
//
// (*) Display link is started before UI thread because FlutterVSyncWaiter will run display
// link for one tick at the beginning to determine vsync phase.
//
// http://www.openradar.me/radar?id=5520107644125184
CVDisplayLinkRef retained = CVDisplayLinkRetain(display_link);
[NSThread detachNewThreadWithBlock:^{
CVDisplayLinkStart(retained);
CVDisplayLinkRelease(retained);
}];
}
} else if (!should_be_running && is_running) {
CVDisplayLinkStop(display_link);
}
}
void DisplayLinkManager::UnregisterDisplayLink(_FlutterDisplayLink* display_link) {
std::unique_lock<std::mutex> lock(mutex_);
for (auto entry = entries_.begin(); entry != entries_.end(); ++entry) {
auto it = std::find(entry->clients.begin(), entry->clients.end(), display_link);
if (it != entry->clients.end()) {
entry->clients.erase(it);
if (entry->clients.empty()) {
// Erasing the entry - take the display link instance and stop / release it
// outside of the mutex.
CVDisplayLinkRef display_link = entry->display_link_locked;
entries_.erase(entry);
lock.unlock();
CVDisplayLinkStop(display_link);
CVDisplayLinkRelease(display_link);
} else {
// Update the display link state outside of the mutex.
bool should_be_running = entry->ShouldBeRunning();
CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry->display_link_locked);
lock.unlock();
RunOrStopDisplayLink(display_link, should_be_running);
CVDisplayLinkRelease(display_link);
}
return;
}
}
}
void DisplayLinkManager::RegisterDisplayLink(_FlutterDisplayLink* display_link,
CGDirectDisplayID display_id) {
std::unique_lock<std::mutex> lock(mutex_);
for (ScreenEntry& entry : entries_) {
if (entry.display_id == display_id) {
entry.clients.push_back(display_link);
bool should_be_running = entry.ShouldBeRunning();
CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry.display_link_locked);
lock.unlock();
RunOrStopDisplayLink(display_link, should_be_running);
CVDisplayLinkRelease(display_link);
return;
}
}
ScreenEntry entry;
entry.display_id = display_id;
entry.clients.push_back(display_link);
CVDisplayLinkCreateWithCGDisplay(display_id, &entry.display_link_locked);
CVDisplayLinkSetOutputHandler(
entry.display_link_locked,
^(CVDisplayLinkRef display_link, const CVTimeStamp* in_now, const CVTimeStamp* in_output_time,
CVOptionFlags flags_in, CVOptionFlags* flags_out) {
OnDisplayLink(display_link, in_now, in_output_time, flags_in, flags_out);
return 0;
});
// This is a new display link so it is safe to start it with mutex held.
bool should_be_running = entry.ShouldBeRunning();
RunOrStopDisplayLink(entry.display_link_locked, should_be_running);
entries_.push_back(entry);
}
void DisplayLinkManager::PausedDidChange(_FlutterDisplayLink* display_link) {
std::unique_lock<std::mutex> lock(mutex_);
for (ScreenEntry& entry : entries_) {
auto it = std::find(entry.clients.begin(), entry.clients.end(), display_link);
if (it != entry.clients.end()) {
bool running = entry.ShouldBeRunning();
CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry.display_link_locked);
lock.unlock();
RunOrStopDisplayLink(display_link, running);
CVDisplayLinkRelease(display_link);
return;
}
}
}
CFTimeInterval DisplayLinkManager::GetNominalOutputPeriod(CGDirectDisplayID display_id) {
std::unique_lock<std::mutex> lock(mutex_);
for (ScreenEntry& entry : entries_) {
if (entry.display_id == display_id) {
CVDisplayLinkRef display_link = CVDisplayLinkRetain(entry.display_link_locked);
lock.unlock();
CVTime latency = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link);
CVDisplayLinkRelease(display_link);
return (CFTimeInterval)latency.timeValue / (CFTimeInterval)latency.timeScale;
}
}
return 0;
}
void DisplayLinkManager::OnDisplayLink(CVDisplayLinkRef display_link,
const CVTimeStamp* in_now,
const CVTimeStamp* in_output_time,
CVOptionFlags flags_in,
CVOptionFlags* flags_out) {
// Hold the mutex only while copying clients.
std::vector<_FlutterDisplayLink*> clients;
{
std::lock_guard<std::mutex> lock(mutex_);
for (ScreenEntry& entry : entries_) {
if (entry.display_link_locked == display_link) {
clients = entry.clients;
break;
}
}
}
CFTimeInterval timestamp = (CFTimeInterval)in_now->hostTime / CVGetHostClockFrequency();
CFTimeInterval target_timestamp =
(CFTimeInterval)in_output_time->hostTime / CVGetHostClockFrequency();
for (_FlutterDisplayLink* client : clients) {
[client didFireWithTimestamp:timestamp targetTimestamp:target_timestamp];
}
}
} // namespace
@interface _FlutterDisplayLinkView : NSView {
}
@end
static NSString* const kFlutterDisplayLinkViewDidMoveToWindow =
@"FlutterDisplayLinkViewDidMoveToWindow";
@implementation _FlutterDisplayLinkView
- (void)viewDidMoveToWindow {
[super viewDidMoveToWindow];
[[NSNotificationCenter defaultCenter] postNotificationName:kFlutterDisplayLinkViewDidMoveToWindow
object:self];
}
@end
@implementation _FlutterDisplayLink
@synthesize delegate = _delegate;
- (instancetype)initWithView:(NSView*)view {
FML_DCHECK([NSThread isMainThread]);
if (self = [super init]) {
self->_view = [[_FlutterDisplayLinkView alloc] initWithFrame:CGRectZero];
[view addSubview:self->_view];
_paused = YES;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(viewDidChangeWindow:)
name:kFlutterDisplayLinkViewDidMoveToWindow
object:self->_view];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidChangeScreen:)
name:NSWindowDidChangeScreenNotification
object:nil];
[self updateScreen];
}
return self;
}
- (void)invalidate {
@synchronized(self) {
FML_DCHECK([NSThread isMainThread]);
[_view removeFromSuperview];
[[NSNotificationCenter defaultCenter] removeObserver:self];
_view = nil;
_delegate = nil;
}
DisplayLinkManager::Instance().UnregisterDisplayLink(self);
}
- (void)updateScreen {
DisplayLinkManager::Instance().UnregisterDisplayLink(self);
std::optional<CGDirectDisplayID> displayId;
@synchronized(self) {
NSScreen* screen = _view.window.screen;
if (screen != nil) {
// https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc
_display_id = (CGDirectDisplayID)[
[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
} else {
_display_id = std::nullopt;
}
displayId = _display_id;
}
if (displayId.has_value()) {
DisplayLinkManager::Instance().RegisterDisplayLink(self, *displayId);
}
}
- (void)viewDidChangeWindow:(NSNotification*)notification {
NSView* view = notification.object;
if (_view == view) {
[self updateScreen];
}
}
- (void)windowDidChangeScreen:(NSNotification*)notification {
NSWindow* window = notification.object;
if (_view.window == window) {
[self updateScreen];
}
}
- (void)didFireWithTimestamp:(CFTimeInterval)timestamp
targetTimestamp:(CFTimeInterval)targetTimestamp {
@synchronized(self) {
if (!_paused) {
id<FlutterDisplayLinkDelegate> delegate = _delegate;
[delegate onDisplayLink:timestamp targetTimestamp:targetTimestamp];
}
}
}
- (BOOL)paused {
@synchronized(self) {
return _paused;
}
}
- (void)setPaused:(BOOL)paused {
@synchronized(self) {
if (_paused == paused) {
return;
}
_paused = paused;
}
DisplayLinkManager::Instance().PausedDidChange(self);
}
- (CFTimeInterval)nominalOutputRefreshPeriod {
CGDirectDisplayID display_id;
@synchronized(self) {
if (_display_id.has_value()) {
display_id = *_display_id;
} else {
return 0;
}
}
return DisplayLinkManager::Instance().GetNominalOutputPeriod(display_id);
}
@end
@implementation FlutterDisplayLink
+ (instancetype)displayLinkWithView:(NSView*)view {
return [[_FlutterDisplayLink alloc] initWithView:view];
}
- (void)invalidate {
[self doesNotRecognizeSelector:_cmd];
}
@end

View File

@ -0,0 +1,150 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
#import <AppKit/AppKit.h>
#include "flutter/fml/synchronization/waitable_event.h"
#include "flutter/testing/testing.h"
@interface TestDisplayLinkDelegate : NSObject <FlutterDisplayLinkDelegate> {
void (^_block)(CFTimeInterval timestamp, CFTimeInterval targetTimestamp);
}
- (instancetype)initWithBlock:(void (^)(CFTimeInterval timestamp,
CFTimeInterval targetTimestamp))block;
@end
@implementation TestDisplayLinkDelegate
- (instancetype)initWithBlock:(void (^__strong)(CFTimeInterval, CFTimeInterval))block {
if (self = [super init]) {
_block = block;
}
return self;
}
- (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp {
_block(timestamp, targetTimestamp);
}
@end
TEST(FlutterDisplayLinkTest, ViewAddedToWindowFirst) {
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
styleMask:NSWindowStyleMaskTitled
backing:NSBackingStoreNonretained
defer:NO];
NSView* view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
[window setContentView:view];
auto event = std::make_shared<fml::AutoResetWaitableEvent>();
TestDisplayLinkDelegate* delegate = [[TestDisplayLinkDelegate alloc]
initWithBlock:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp) {
event->Signal();
}];
FlutterDisplayLink* displayLink = [FlutterDisplayLink displayLinkWithView:view];
displayLink.delegate = delegate;
displayLink.paused = NO;
event->Wait();
[displayLink invalidate];
}
TEST(FlutterDisplayLinkTest, ViewAddedToWindowLater) {
NSView* view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
auto event = std::make_shared<fml::AutoResetWaitableEvent>();
TestDisplayLinkDelegate* delegate = [[TestDisplayLinkDelegate alloc]
initWithBlock:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp) {
event->Signal();
}];
FlutterDisplayLink* displayLink = [FlutterDisplayLink displayLinkWithView:view];
displayLink.delegate = delegate;
displayLink.paused = NO;
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
styleMask:NSWindowStyleMaskTitled
backing:NSBackingStoreNonretained
defer:NO];
[window setContentView:view];
event->Wait();
[displayLink invalidate];
}
TEST(FlutterDisplayLinkTest, ViewRemovedFromWindow) {
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
styleMask:NSWindowStyleMaskTitled
backing:NSBackingStoreNonretained
defer:NO];
NSView* view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
[window setContentView:view];
auto event = std::make_shared<fml::AutoResetWaitableEvent>();
TestDisplayLinkDelegate* delegate = [[TestDisplayLinkDelegate alloc]
initWithBlock:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp) {
event->Signal();
}];
FlutterDisplayLink* displayLink = [FlutterDisplayLink displayLinkWithView:view];
displayLink.delegate = delegate;
displayLink.paused = NO;
event->Wait();
displayLink.paused = YES;
event->Reset();
displayLink.paused = NO;
[window setContentView:nil];
EXPECT_TRUE(event->WaitWithTimeout(fml::TimeDelta::FromMilliseconds(100)));
EXPECT_FALSE(event->IsSignaledForTest());
[displayLink invalidate];
}
TEST(FlutterDisplayLinkTest, WorkaroundForFB13482573) {
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
styleMask:NSWindowStyleMaskTitled
backing:NSBackingStoreNonretained
defer:NO];
NSView* view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
[window setContentView:view];
auto event = std::make_shared<fml::AutoResetWaitableEvent>();
TestDisplayLinkDelegate* delegate = [[TestDisplayLinkDelegate alloc]
initWithBlock:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp) {
event->Signal();
}];
FlutterDisplayLink* displayLink = [FlutterDisplayLink displayLinkWithView:view];
displayLink.delegate = delegate;
displayLink.paused = NO;
event->Wait();
displayLink.paused = YES;
event->Reset();
[NSThread detachNewThreadWithBlock:^{
// Here pthread_self() will be same as pthread_self inside first invocation of
// display link callback, causing CVDisplayLinkStart to return error.
displayLink.paused = NO;
}];
event->Wait();
[displayLink invalidate];
}

View File

@ -18,10 +18,12 @@
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMenuPlugin.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.h"
@ -459,12 +461,31 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
// Proxy to allow plugins, channels to hold a weak reference to the binary messenger (self).
FlutterBinaryMessengerRelay* _binaryMessenger;
// Map from ViewId to vsync waiter. Note that this is modified on main thread
// but accessed on UI thread, so access must be @synchronized.
NSMapTable<NSNumber*, FlutterVSyncWaiter*>* _vsyncWaiters;
}
- (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project {
return [self initWithName:labelPrefix project:project allowHeadlessExecution:YES];
}
static const int kMainThreadPriority = 47;
static void SetThreadPriority(FlutterThreadPriority priority) {
if (priority == kDisplay || priority == kRaster) {
pthread_t thread = pthread_self();
sched_param param;
int policy;
if (!pthread_getschedparam(thread, &policy, &param)) {
param.sched_priority = kMainThreadPriority;
pthread_setschedparam(thread, policy, &param);
}
pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
}
}
- (instancetype)initWithName:(NSString*)labelPrefix
project:(FlutterDartProject*)project
allowHeadlessExecution:(BOOL)allowHeadlessExecution {
@ -515,6 +536,8 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
_terminationHandler = nil;
}
_vsyncWaiters = [NSMapTable strongToStrongObjectsMapTable];
return self;
}
@ -624,7 +647,7 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
const FlutterCustomTaskRunners custom_task_runners = {
.struct_size = sizeof(FlutterCustomTaskRunners),
.platform_task_runner = &cocoa_task_runner_description,
};
.thread_priority_setter = SetThreadPriority};
flutterArguments.custom_task_runners = &custom_task_runners;
[self loadAOTData:_project.assetsPath];
@ -639,6 +662,11 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
[engine engineCallbackOnPreEngineRestart];
};
flutterArguments.vsync_callback = [](void* user_data, intptr_t baton) {
FlutterEngine* engine = (__bridge FlutterEngine*)user_data;
[engine onVSync:baton];
};
FlutterRendererConfig rendererConfig = [_renderer createRendererConfig];
FlutterEngineResult result = _embedderAPI.Initialize(
FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine);
@ -703,6 +731,36 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
[controller setUpWithEngine:self viewId:viewId threadSynchronizer:_threadSynchronizer];
NSAssert(controller.viewId == viewId, @"Failed to assign view ID.");
[_viewControllers setObject:controller forKey:@(viewId)];
if (controller.viewLoaded) {
[self viewControllerViewDidLoad:controller];
}
}
- (void)viewControllerViewDidLoad:(FlutterViewController*)viewController {
__weak FlutterEngine* weakSelf = self;
FlutterVSyncWaiter* waiter = [[FlutterVSyncWaiter alloc]
initWithDisplayLink:[FlutterDisplayLink displayLinkWithView:viewController.view]
block:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp,
uintptr_t baton) {
// CAMediaTime and flutter time are both mach_absolute_time.
uint64_t timeNanos = timestamp * 1000000000;
uint64_t targetTimeNanos = targetTimestamp * 1000000000;
FlutterEngine* engine = weakSelf;
if (engine) {
// It is a bit unfortunate that embedder requires OnVSync call on
// platform thread just to immediately redispatch it to UI thread.
// We are already on UI thread right now, but have to do the
// extra hop to main thread.
[engine->_threadSynchronizer performOnPlatformThread:^{
engine->_embedderAPI.OnVsync(_engine, baton, timeNanos, targetTimeNanos);
}];
}
}];
FML_DCHECK([_vsyncWaiters objectForKey:@(viewController.viewId)] == nil);
@synchronized(_vsyncWaiters) {
[_vsyncWaiters setObject:waiter forKey:@(viewController.viewId)];
}
}
- (void)deregisterViewControllerForId:(FlutterViewId)viewId {
@ -711,6 +769,9 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
[oldController detachFromEngine];
[_viewControllers removeObjectForKey:@(viewId)];
}
@synchronized(_vsyncWaiters) {
[_vsyncWaiters removeObjectForKey:@(viewId)];
}
}
- (void)shutDownIfNeeded {
@ -1034,6 +1095,14 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
}
}
- (void)onVSync:(uintptr_t)baton {
@synchronized(_vsyncWaiters) {
// TODO(knopp): Use vsync waiter for correct view.
FlutterVSyncWaiter* waiter = [_vsyncWaiters objectForKey:@(kFlutterImplicitViewId)];
[waiter waitForVSync:baton];
}
}
/**
* Note: Called from dealloc. Should not use accessors or other methods.
*/

View File

@ -508,6 +508,7 @@ TEST_F(FlutterEngineTest, Compositor) {
nibName:nil
bundle:nil];
[viewController loadView];
[viewController viewDidLoad];
viewController.flutterView.frame = CGRectMake(0, 0, 800, 600);
EXPECT_TRUE([engine runWithEntrypoint:@"canCompositePlatformViews"]);

View File

@ -136,6 +136,11 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) {
*/
- (void)addViewController:(FlutterViewController*)viewController;
/**
* Notify the engine that a view for the given view controller has been loaded.
*/
- (void)viewControllerViewDidLoad:(FlutterViewController*)viewController;
/**
* Dissociate the given view controller from this engine.
*

View File

@ -69,8 +69,9 @@
* and can be used to perform additional work, such as mutating platform views. It is guaranteed be
* called in the same CATransaction.
*/
- (void)present:(nonnull NSArray<FlutterSurfacePresentInfo*>*)surfaces
notify:(nullable dispatch_block_t)notify;
- (void)presentSurfaces:(nonnull NSArray<FlutterSurfacePresentInfo*>*)surfaces
atTime:(CFTimeInterval)presentationTime
notify:(nullable dispatch_block_t)notify;
@end

View File

@ -34,6 +34,8 @@
// FLTEnableSurfaceDebugInfo value in main bundle Info.plist.
NSNumber* _enableSurfaceDebugInfo;
CATextLayer* _infoLayer;
CFTimeInterval _lastPresentationTime;
}
/**
@ -213,11 +215,36 @@ static CGSize GetRequiredFrameSize(NSArray<FlutterSurfacePresentInfo*>* surfaces
return size;
}
- (void)present:(NSArray<FlutterSurfacePresentInfo*>*)surfaces notify:(dispatch_block_t)notify {
- (void)presentSurfaces:(NSArray<FlutterSurfacePresentInfo*>*)surfaces
atTime:(CFTimeInterval)presentationTime
notify:(dispatch_block_t)notify {
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
[commandBuffer commit];
[commandBuffer waitUntilScheduled];
if (presentationTime > 0) {
// Enforce frame pacing. It seems that the target timestamp of CVDisplayLink does not
// exactly correspond to core animation deadline. Especially with 120hz, setting the frame
// contents too close after previous target timestamp will result in uneven frame pacing.
// Empirically setting the content in the second half of frame interval seems to work
// well for both 60hz and 120hz.
//
// The easiest way to ensure that the content is not set too early is to delay raster thread.
// At this point raster thread should be idle (the next frame vsync has not been signalled yet).
// This will show on a timeline as "FlutterCompositionPresentLayers" but should not cause jank
// because the waiting interval is calculated relative to presentation time.
//
// Alternative to blocking raster thread would be to copy all presentation info provided by
// embedder and schedule a presentation timer. This would require additional coordination with
// FlutterThreadSynchronizer.
CFTimeInterval minPresentationTime = (presentationTime + _lastPresentationTime) / 2.0;
CFTimeInterval now = CACurrentMediaTime();
if (now < minPresentationTime) {
[NSThread sleepForTimeInterval:minPresentationTime - now];
}
}
_lastPresentationTime = presentationTime;
// Get the actual dimensions of the frame (relevant for thread synchronizer).
CGSize size = GetRequiredFrameSize(surfaces);

View File

@ -99,17 +99,17 @@ TEST(FlutterSurfaceManager, BackBufferCacheDoesNotLeak) {
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
[surfaceManager present:@[ CreatePresentInfo(surface1) ] notify:nil];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(110, 110)];
[surfaceManager present:@[ CreatePresentInfo(surface2) ] notify:nil];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(120, 120)];
[surfaceManager present:@[ CreatePresentInfo(surface3) ] notify:nil];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface3) ] atTime:0 notify:nil];
// Cache should be cleaned during present and only contain the last visible
// surface(s).
@ -117,10 +117,10 @@ TEST(FlutterSurfaceManager, BackBufferCacheDoesNotLeak) {
auto surfaceFromCache = [surfaceManager surfaceForSize:CGSizeMake(110, 110)];
EXPECT_EQ(surfaceFromCache, surface2);
[surfaceManager present:@[] notify:nil];
[surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
[surfaceManager present:@[] notify:nil];
[surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
}
@ -138,7 +138,7 @@ TEST(FlutterSurfaceManager, SurfacesAreRecycled) {
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
EXPECT_EQ(surfaceManager.frontSurfaces.count, 0ul);
[surfaceManager present:@[ CreatePresentInfo(surface1) ] notify:nil];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
EXPECT_EQ(surfaceManager.frontSurfaces.count, 1ul);
@ -151,7 +151,7 @@ TEST(FlutterSurfaceManager, SurfacesAreRecycled) {
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
[surfaceManager present:@[ CreatePresentInfo(surface2) ] notify:nil];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil];
// Check that current front surface returns to cache.
EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
@ -174,14 +174,16 @@ TEST(FlutterSurfaceManager, LayerManagement) {
EXPECT_EQ(testView.layer.sublayers.count, 0ul);
auto surface1_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
[surfaceManager present:@[ CreatePresentInfo(surface1_1, CGPointMake(20, 10)) ] notify:nil];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1_1, CGPointMake(20, 10)) ]
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 1ul);
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 40)));
auto surface2_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
auto surface2_2 = [surfaceManager surfaceForSize:CGSizeMake(20, 20)];
[surfaceManager present:@[
[surfaceManager presentSurfaces:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
@ -189,7 +191,8 @@ TEST(FlutterSurfaceManager, LayerManagement) {
FlutterRect{40, 0, 60, 20},
})
]
notify:nil];
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 2ul);
EXPECT_EQ(testView.layer.sublayers[0].zPosition, 1.0);
@ -208,14 +211,15 @@ TEST(FlutterSurfaceManager, LayerManagement) {
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 70)));
// Check second overlay sublayer is removed while first is reused and updated
[surfaceManager present:@[
[surfaceManager presentSurfaces:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
FlutterRect{0, 10, 20, 20},
})
]
notify:nil];
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 2ul);
{
NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
@ -225,7 +229,7 @@ TEST(FlutterSurfaceManager, LayerManagement) {
}
// Check that second overlay sublayer is added back while first is reused and updated
[surfaceManager present:@[
[surfaceManager presentSurfaces:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
@ -233,7 +237,8 @@ TEST(FlutterSurfaceManager, LayerManagement) {
FlutterRect{40, 0, 60, 20},
})
]
notify:nil];
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 2ul);
{
@ -246,13 +251,15 @@ TEST(FlutterSurfaceManager, LayerManagement) {
}
auto surface3_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
[surfaceManager present:@[ CreatePresentInfo(surface3_1, CGPointMake(20, 10)) ] notify:nil];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface3_1, CGPointMake(20, 10)) ]
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 1ul);
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 40)));
// Check removal of all surfaces.
[surfaceManager present:@[] notify:nil];
[surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 0ul);
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(0, 0)));
}

View File

@ -41,6 +41,13 @@
size:(CGSize)size
notify:(nonnull dispatch_block_t)notify;
/**
* Schedules the given block to be performed on the platform thread.
* The block will be performed even if the platform thread is blocked waiting
* for a commit.
*/
- (void)performOnPlatformThread:(nonnull dispatch_block_t)block;
/**
* Requests the synchronizer to track another view.
*

View File

@ -164,6 +164,19 @@
event.Wait();
}
- (void)performOnPlatformThread:(nonnull dispatch_block_t)block {
std::unique_lock<std::mutex> lock(_mutex);
_scheduledBlocks.push_back(block);
if (_beginResizeWaiting) {
_condBlockBeginResize.notify_all();
} else {
dispatch_async(_mainQueue, ^{
std::unique_lock<std::mutex> lock(_mutex);
[self drain];
});
}
}
- (void)registerView:(int64_t)viewId {
dispatch_assert_queue(_mainQueue);
std::unique_lock<std::mutex> lock(_mutex);

View File

@ -7,12 +7,6 @@
#import "flutter/fml/synchronization/waitable_event.h"
#import "flutter/testing/testing.h"
namespace flutter::testing {
namespace {} // namespace
} // namespace flutter::testing
@interface FlutterThreadSynchronizerTestScaffold : NSObject
@property(nonatomic, readonly, nonnull) FlutterThreadSynchronizer* synchronizer;

View File

@ -0,0 +1,26 @@
#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVSYNCWAITER_H_
#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVSYNCWAITER_H_
#import <AppKit/AppKit.h>
@class FlutterDisplayLink;
@interface FlutterVSyncWaiter : NSObject
/// Creates new waiter instance tied to provided NSView.
/// This function must be called on the main thread.
///
/// Provided |block| will be invoked on same thread as -waitForVSync:.
- (instancetype)initWithDisplayLink:(FlutterDisplayLink*)displayLink
block:(void (^)(CFTimeInterval timestamp,
CFTimeInterval targetTimestamp,
uintptr_t baton))block;
/// Schedules |baton| to be signaled on next display refresh.
/// The block provided in the initializer will be invoked on same thread
/// as this method (there must be a run loop associated with current thread).
- (void)waitForVSync:(uintptr_t)baton;
@end
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVSYNCWAITER_H_

View File

@ -0,0 +1,172 @@
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
#include "flutter/fml/logging.h"
#include <optional>
#include <vector>
#if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_PROFILE)
#define VSYNC_TRACING_ENABLED 1
#endif
#if VSYNC_TRACING_ENABLED
#include <OSLog/OSLog.h>
// Trace vsync events using os_signpost so that they can be seen in Instruments "Points of
// Interest".
#define TRACE_VSYNC(event_type, baton) \
do { \
os_log_t log = os_log_create("FlutterVSync", "PointsOfInterest"); \
os_signpost_event_emit(log, OS_SIGNPOST_ID_EXCLUSIVE, event_type, "baton %lx", baton); \
} while (0)
#else
#define TRACE_VSYNC(event_type, baton) \
do { \
} while (0)
#endif
@interface FlutterVSyncWaiter () <FlutterDisplayLinkDelegate>
@end
// It's preferable to fire the timers slightly early than too late due to scheduling latency.
// 1ms before vsync should be late enough for all events to be processed.
static const CFTimeInterval kTimerLatencyCompensation = 0.001;
@implementation FlutterVSyncWaiter {
std::optional<std::uintptr_t> _pending_baton;
FlutterDisplayLink* _displayLink;
void (^_block)(CFTimeInterval, CFTimeInterval, uintptr_t);
NSRunLoop* _runLoop;
CFTimeInterval _lastTargetTimestamp;
}
- (instancetype)initWithDisplayLink:(FlutterDisplayLink*)displayLink
block:(void (^)(CFTimeInterval timestamp,
CFTimeInterval targetTimestamp,
uintptr_t baton))block {
FML_DCHECK([NSThread isMainThread]);
if (self = [super init]) {
_block = block;
_displayLink = displayLink;
_displayLink.delegate = self;
// Get at least one callback to initialize _lastTargetTimestamp.
_displayLink.paused = NO;
}
return self;
}
// Called on same thread as the vsync request (UI thread).
- (void)processDisplayLink:(CFTimeInterval)timestamp
targetTimestamp:(CFTimeInterval)targetTimestamp {
FML_DCHECK([NSRunLoop currentRunLoop] == _runLoop);
_lastTargetTimestamp = targetTimestamp;
// CVDisplayLink callback is called one and a half frame before the target
// timestamp. That can cause frame-pacing issues if the frame is rendered too early,
// it may also trigger frame start before events are processed.
CFTimeInterval minStart = targetTimestamp - _displayLink.nominalOutputRefreshPeriod;
CFTimeInterval current = CACurrentMediaTime();
CFTimeInterval remaining = std::max(minStart - current - kTimerLatencyCompensation, 0.0);
TRACE_VSYNC("DisplayLinkCallback-Original", _pending_baton.value_or(0));
NSTimer* timer = [NSTimer
timerWithTimeInterval:remaining
repeats:NO
block:^(NSTimer* _Nonnull timer) {
if (!_pending_baton.has_value()) {
TRACE_VSYNC("DisplayLinkPaused", size_t(0));
_displayLink.paused = YES;
return;
}
TRACE_VSYNC("DisplayLinkCallback-Delayed", _pending_baton.value_or(0));
_block(minStart, targetTimestamp, *_pending_baton);
_pending_baton = std::nullopt;
}];
[_runLoop addTimer:timer forMode:NSRunLoopCommonModes];
}
// Called from display link thread.
- (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp {
@synchronized(self) {
if (_runLoop == nil) {
// Initial vsync - timestamp will be used to determine vsync phase.
_lastTargetTimestamp = targetTimestamp;
_displayLink.paused = YES;
} else {
[_runLoop performBlock:^{
[self processDisplayLink:timestamp targetTimestamp:targetTimestamp];
}];
}
}
}
// Called from UI thread.
- (void)waitForVSync:(uintptr_t)baton {
// RunLoop is accessed both from main thread and from the display link thread.
@synchronized(self) {
if (_runLoop == nil) {
_runLoop = [NSRunLoop currentRunLoop];
}
}
FML_DCHECK(_runLoop == [NSRunLoop currentRunLoop]);
if (_pending_baton.has_value()) {
FML_LOG(WARNING) << "Engine requested vsync while another was pending";
_block(0, 0, *_pending_baton);
_pending_baton = std::nullopt;
}
TRACE_VSYNC("VSyncRequest", _pending_baton.value_or(0));
CFTimeInterval tick_interval = _displayLink.nominalOutputRefreshPeriod;
if (_displayLink.paused || tick_interval == 0) {
// When starting display link the first notification will come in the middle
// of next frame, which would incur a whole frame period of latency.
// To avoid that, first vsync notification will be fired using a timer
// scheduled to fire where the next frame is expected to start.
// Also use a timer if display link does not belong to any display
// (nominalOutputRefreshPeriod being 0)
// Start of the vsync interval.
CFTimeInterval start = CACurrentMediaTime();
// Timer delay is calculated as the time to the next frame start.
CFTimeInterval delay = 0;
if (tick_interval != 0 && _lastTargetTimestamp != 0) {
CFTimeInterval phase = fmod(_lastTargetTimestamp, tick_interval);
CFTimeInterval now = start;
start = now - (fmod(now, tick_interval)) + phase;
if (start < now) {
start += tick_interval;
}
delay = std::max(start - now - kTimerLatencyCompensation, 0.0);
}
NSTimer* timer = [NSTimer timerWithTimeInterval:delay
repeats:NO
block:^(NSTimer* timer) {
CFTimeInterval targetTimestamp =
start + tick_interval;
TRACE_VSYNC("SynthesizedInitialVSync", baton);
_block(start, targetTimestamp, baton);
}];
[_runLoop addTimer:timer forMode:NSRunLoopCommonModes];
_displayLink.paused = NO;
} else {
_pending_baton = baton;
}
}
- (void)dealloc {
if (_pending_baton.has_value()) {
FML_LOG(WARNING) << "Deallocating FlutterVSyncWaiter with a pending vsync";
}
[_displayLink invalidate];
}
@end

View File

@ -0,0 +1,170 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h"
#import "flutter/testing/testing.h"
@interface TestDisplayLink : FlutterDisplayLink {
}
@property(nonatomic) CFTimeInterval nominalOutputRefreshPeriod;
@end
@implementation TestDisplayLink
@synthesize nominalOutputRefreshPeriod = _nominalOutputRefreshPeriod;
@synthesize delegate = _delegate;
@synthesize paused = _paused;
- (instancetype)init {
if (self = [super init]) {
_paused = YES;
}
return self;
}
- (void)tickWithTimestamp:(CFTimeInterval)timestamp
targetTimestamp:(CFTimeInterval)targetTimestamp {
[_delegate onDisplayLink:timestamp targetTimestamp:targetTimestamp];
}
- (void)invalidate {
}
@end
TEST(FlutterVSyncWaiterTest, RequestsInitialVSync) {
TestDisplayLink* displayLink = [[TestDisplayLink alloc] init];
EXPECT_TRUE(displayLink.paused);
// When created waiter requests a reference vsync to determine vsync phase.
FlutterVSyncWaiter* waiter = [[FlutterVSyncWaiter alloc]
initWithDisplayLink:displayLink
block:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp,
uintptr_t baton){
}];
(void)waiter;
EXPECT_FALSE(displayLink.paused);
[displayLink tickWithTimestamp:CACurrentMediaTime()
targetTimestamp:CACurrentMediaTime() + 1.0 / 60.0];
EXPECT_TRUE(displayLink.paused);
}
static void BusyWait(CFTimeInterval duration) {
CFTimeInterval start = CACurrentMediaTime();
while (CACurrentMediaTime() < start + duration) {
}
}
// See FlutterVSyncWaiter.mm for the original definition.
static const CFTimeInterval kTimerLatencyCompensation = 0.001;
TEST(FlutterVSyncWaiterTest, FirstVSyncIsSynthesized) {
TestDisplayLink* displayLink = [[TestDisplayLink alloc] init];
displayLink.nominalOutputRefreshPeriod = 1.0 / 60.0;
auto test = [&](CFTimeInterval waitDuration, CFTimeInterval expectedDelay) {
__block CFTimeInterval timestamp = 0;
__block CFTimeInterval targetTimestamp = 0;
__block size_t baton = 0;
FlutterVSyncWaiter* waiter = [[FlutterVSyncWaiter alloc]
initWithDisplayLink:displayLink
block:^(CFTimeInterval _timestamp, CFTimeInterval _targetTimestamp,
uintptr_t _baton) {
timestamp = _timestamp;
targetTimestamp = _targetTimestamp;
baton = _baton;
EXPECT_TRUE(CACurrentMediaTime() >= _timestamp - kTimerLatencyCompensation);
CFRunLoopStop(CFRunLoopGetCurrent());
}];
// Reference vsync to setup phase.
CFTimeInterval now = CACurrentMediaTime();
// CVDisplayLink callback is called one and a half frame before the target.
[displayLink tickWithTimestamp:now + 0.5 * displayLink.nominalOutputRefreshPeriod
targetTimestamp:now + 2 * displayLink.nominalOutputRefreshPeriod];
EXPECT_EQ(displayLink.paused, YES);
// Vsync was not requested yet, block should not have been called.
EXPECT_EQ(timestamp, 0);
BusyWait(waitDuration);
// Synthesized vsync should come in 1/60th of a second after the first.
CFTimeInterval expectedTimestamp = now + expectedDelay;
[waiter waitForVSync:1];
CFRunLoopRun();
EXPECT_DOUBLE_EQ(timestamp, expectedTimestamp);
EXPECT_DOUBLE_EQ(targetTimestamp, expectedTimestamp + displayLink.nominalOutputRefreshPeriod);
EXPECT_EQ(baton, size_t(1));
};
// First argument if the wait duration after reference vsync.
// Second argument is the expected delay between reference vsync and synthesized vsync.
test(0.005, displayLink.nominalOutputRefreshPeriod);
test(0.025, 2 * displayLink.nominalOutputRefreshPeriod);
test(0.040, 3 * displayLink.nominalOutputRefreshPeriod);
}
TEST(FlutterVSyncWaiterTest, VSyncWorks) {
TestDisplayLink* displayLink = [[TestDisplayLink alloc] init];
displayLink.nominalOutputRefreshPeriod = 1.0 / 60.0;
struct Entry {
CFTimeInterval timestamp;
CFTimeInterval targetTimestamp;
size_t baton;
};
__block std::vector<Entry> entries;
FlutterVSyncWaiter* waiter = [[FlutterVSyncWaiter alloc]
initWithDisplayLink:displayLink
block:^(CFTimeInterval timestamp, CFTimeInterval targetTimestamp,
uintptr_t baton) {
entries.push_back({timestamp, targetTimestamp, baton});
EXPECT_TRUE(CACurrentMediaTime() >= timestamp - kTimerLatencyCompensation);
CFRunLoopStop(CFRunLoopGetCurrent());
}];
// Reference vsync to setup phase.
CFTimeInterval now = CACurrentMediaTime();
// CVDisplayLink callback is called one and a half frame before the target.
[displayLink tickWithTimestamp:now + 0.5 * displayLink.nominalOutputRefreshPeriod
targetTimestamp:now + 2 * displayLink.nominalOutputRefreshPeriod];
EXPECT_EQ(displayLink.paused, YES);
[waiter waitForVSync:1];
CFRunLoopRun();
[waiter waitForVSync:2];
[displayLink tickWithTimestamp:now + 1.5 * displayLink.nominalOutputRefreshPeriod
targetTimestamp:now + 3 * displayLink.nominalOutputRefreshPeriod];
CFRunLoopRun();
[waiter waitForVSync:3];
[displayLink tickWithTimestamp:now + 2.5 * displayLink.nominalOutputRefreshPeriod
targetTimestamp:now + 4 * displayLink.nominalOutputRefreshPeriod];
CFRunLoopRun();
EXPECT_FALSE(displayLink.paused);
// Vsync without baton should pause the display link.
[displayLink tickWithTimestamp:now + 3.5 * displayLink.nominalOutputRefreshPeriod
targetTimestamp:now + 5 * displayLink.nominalOutputRefreshPeriod];
// Make sure to run the timer scheduled in display link callback.
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.02, NO);
ASSERT_TRUE(displayLink.paused);
EXPECT_EQ(entries.size(), size_t(3));
EXPECT_DOUBLE_EQ(entries[0].timestamp, now + displayLink.nominalOutputRefreshPeriod);
EXPECT_DOUBLE_EQ(entries[0].targetTimestamp, now + 2 * displayLink.nominalOutputRefreshPeriod);
EXPECT_EQ(entries[0].baton, size_t(1));
EXPECT_DOUBLE_EQ(entries[1].timestamp, now + 2 * displayLink.nominalOutputRefreshPeriod);
EXPECT_DOUBLE_EQ(entries[1].targetTimestamp, now + 3 * displayLink.nominalOutputRefreshPeriod);
EXPECT_EQ(entries[1].baton, size_t(2));
EXPECT_DOUBLE_EQ(entries[2].timestamp, now + 3 * displayLink.nominalOutputRefreshPeriod);
EXPECT_DOUBLE_EQ(entries[2].targetTimestamp, now + 4 * displayLink.nominalOutputRefreshPeriod);
EXPECT_EQ(entries[2].baton, size_t(3));
}

View File

@ -448,6 +448,7 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine)
[self configureTrackingArea];
[self.view setAllowedTouchTypes:NSTouchTypeMaskIndirect];
[self.view setWantsRestingTouches:YES];
[_engine viewControllerViewDidLoad:self];
}
- (void)viewWillAppear {

View File

@ -1736,6 +1736,10 @@ typedef struct {
/// Extra information for the backing store that the embedder may
/// use during presentation.
FlutterBackingStorePresentInfo* backing_store_present_info;
// Time in nanoseconds at which this frame is scheduled to be presented. 0 if
// not known. See FlutterEngineGetCurrentTime().
uint64_t presentation_time;
} FlutterLayer;
typedef bool (*FlutterBackingStoreCreateCallback)(

View File

@ -479,12 +479,18 @@ void EmbedderExternalViewEmbedder::SubmitFlutterView(
}
{
auto presentation_time_optional = frame->submit_info().presentation_time;
uint64_t presentation_time =
presentation_time_optional.has_value()
? presentation_time_optional->ToEpochDelta().ToNanoseconds()
: 0;
// Submit the scribbled layer to the embedder for presentation.
//
// @warning: Embedder may trample on our OpenGL context here.
EmbedderLayers presented_layers(pending_frame_size_,
pending_device_pixel_ratio_,
pending_surface_transformation_);
EmbedderLayers presented_layers(
pending_frame_size_, pending_device_pixel_ratio_,
pending_surface_transformation_, presentation_time);
builder.PushLayers(presented_layers);

View File

@ -10,10 +10,12 @@ namespace flutter {
EmbedderLayers::EmbedderLayers(SkISize frame_size,
double device_pixel_ratio,
SkMatrix root_surface_transformation)
SkMatrix root_surface_transformation,
uint64_t presentation_time)
: frame_size_(frame_size),
device_pixel_ratio_(device_pixel_ratio),
root_surface_transformation_(root_surface_transformation) {}
root_surface_transformation_(root_surface_transformation),
presentation_time_(presentation_time) {}
EmbedderLayers::~EmbedderLayers() = default;
@ -62,6 +64,7 @@ void EmbedderLayers::PushBackingStoreLayer(
present_info->paint_region = paint_region.get();
regions_referenced_.push_back(std::move(paint_region));
layer.backing_store_present_info = present_info.get();
layer.presentation_time = presentation_time_;
present_info_referenced_.push_back(std::move(present_info));
presented_layers_.push_back(layer);
@ -225,6 +228,8 @@ void EmbedderLayers::PushPlatformViewLayer(
layer.size.width = transformed_layer_bounds.width();
layer.size.height = transformed_layer_bounds.height();
layer.presentation_time = presentation_time_;
presented_layers_.push_back(layer);
}

View File

@ -20,7 +20,8 @@ class EmbedderLayers {
public:
EmbedderLayers(SkISize frame_size,
double device_pixel_ratio,
SkMatrix root_surface_transformation);
SkMatrix root_surface_transformation,
uint64_t presentation_time);
~EmbedderLayers();
@ -48,6 +49,7 @@ class EmbedderLayers {
std::vector<std::unique_ptr<FlutterRegion>> regions_referenced_;
std::vector<std::unique_ptr<std::vector<FlutterRect>>> rects_referenced_;
std::vector<FlutterLayer> presented_layers_;
uint64_t presentation_time_;
FML_DISALLOW_COPY_AND_ASSIGN(EmbedderLayers);
};