mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[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:
parent
75bfeb8fb0
commit
8ff01af723
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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_
|
||||
@ -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
|
||||
@ -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];
|
||||
}
|
||||
@ -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, ¶m)) {
|
||||
param.sched_priority = kMainThreadPriority;
|
||||
pthread_setschedparam(thread, policy, ¶m);
|
||||
}
|
||||
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.
|
||||
*/
|
||||
|
||||
@ -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"]);
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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_
|
||||
@ -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
|
||||
@ -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));
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)(
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user