From 57a040c9391965bd7047dfbc37908a670e267c79 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sun, 7 Jul 2024 15:25:20 +0200 Subject: [PATCH] Allow running macOS embedder with ui and platform thread merged --- ci/licenses_golden/licenses_flutter | 6 +- lib/ui/ui_dart_state.cc | 12 + runtime/dart_isolate.cc | 2 +- shell/platform/darwin/macos/BUILD.gn | 8 +- .../macos/framework/Source/FlutterEngine.mm | 61 ++---- .../framework/Source/FlutterEngineTest.mm | 18 +- .../framework/Source/FlutterEngine_Internal.h | 4 - .../Source/FlutterResizeSynchronizer.h | 40 ++++ .../Source/FlutterResizeSynchronizer.mm | 60 +++++ ...st.mm => FlutterResizeSynchronizerTest.mm} | 4 + .../macos/framework/Source/FlutterRunLoop.h | 46 ++++ .../macos/framework/Source/FlutterRunLoop.mm | 130 +++++++++++ .../framework/Source/FlutterSurfaceManager.h | 6 +- .../framework/Source/FlutterSurfaceManager.mm | 44 ++-- .../Source/FlutterSurfaceManagerTest.mm | 4 +- .../Source/FlutterThreadSynchronizer.h | 112 ---------- .../Source/FlutterThreadSynchronizer.mm | 205 ------------------ .../framework/Source/FlutterVSyncWaiter.h | 4 +- .../framework/Source/FlutterVSyncWaiter.mm | 85 +++----- .../macos/framework/Source/FlutterView.h | 5 +- .../macos/framework/Source/FlutterView.mm | 30 ++- .../framework/Source/FlutterViewController.mm | 13 +- .../Source/FlutterViewController_Internal.h | 3 +- .../macos/framework/Source/FlutterViewTest.mm | 6 - shell/platform/embedder/embedder.h | 4 + .../platform/embedder/embedder_thread_host.cc | 20 +- 26 files changed, 418 insertions(+), 514 deletions(-) create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm rename shell/platform/darwin/macos/framework/Source/{FlutterThreadSynchronizerTest.mm => FlutterResizeSynchronizerTest.mm} (99%) create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterRunLoop.mm delete mode 100644 shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h delete mode 100644 shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index bd215595e427c..12706e14f5fdb 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -43720,9 +43720,9 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTex ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.mm + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h + ../../../flutter/LICENSE -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/FlutterResizeSynchronizer.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizerTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTimeConverter.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTimeConverter.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterUmbrellaImportTests.m + ../../../flutter/LICENSE diff --git a/lib/ui/ui_dart_state.cc b/lib/ui/ui_dart_state.cc index b9a3a9ecf8935..8d2915415efc0 100644 --- a/lib/ui/ui_dart_state.cc +++ b/lib/ui/ui_dart_state.cc @@ -22,6 +22,16 @@ extern void syslog(int, const char*, ...); } #endif +namespace { +static std::set state; +} + +void UglyHackFlushMicrotasks() { + for (auto s : state) { + s->FlushMicrotasksNow(); + } +} + using tonic::ToDart; namespace flutter { @@ -73,10 +83,12 @@ UIDartState::UIDartState( isolate_name_server_(std::move(isolate_name_server)), context_(context) { AddOrRemoveTaskObserver(true /* add */); + state.insert(this); } UIDartState::~UIDartState() { AddOrRemoveTaskObserver(false /* remove */); + state.erase(this); } const std::string& UIDartState::GetAdvisoryScriptURI() const { diff --git a/runtime/dart_isolate.cc b/runtime/dart_isolate.cc index 3dc8403506f7f..2fe1b0e519f98 100644 --- a/runtime/dart_isolate.cc +++ b/runtime/dart_isolate.cc @@ -509,7 +509,7 @@ bool DartIsolate::Initialize(Dart_Isolate dart_isolate) { SetMessageHandlingTaskRunner(GetTaskRunners().GetPlatformTaskRunner(), true); } else { - SetMessageHandlingTaskRunner(GetTaskRunners().GetUITaskRunner(), false); + SetMessageHandlingTaskRunner(GetTaskRunners().GetUITaskRunner(), true); } if (tonic::CheckAndHandleError( diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 401b63bc0b8b6..cc1fe7fb64e17 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -91,6 +91,10 @@ source_set("flutter_framework_source") { "framework/Source/FlutterPlatformViewController.mm", "framework/Source/FlutterRenderer.h", "framework/Source/FlutterRenderer.mm", + "framework/Source/FlutterResizeSynchronizer.h", + "framework/Source/FlutterResizeSynchronizer.mm", + "framework/Source/FlutterRunLoop.h", + "framework/Source/FlutterRunLoop.mm", "framework/Source/FlutterSurface.h", "framework/Source/FlutterSurface.mm", "framework/Source/FlutterSurfaceManager.h", @@ -101,8 +105,6 @@ source_set("flutter_framework_source") { "framework/Source/FlutterTextInputSemanticsObject.mm", "framework/Source/FlutterTextureRegistrar.h", "framework/Source/FlutterTextureRegistrar.mm", - "framework/Source/FlutterThreadSynchronizer.h", - "framework/Source/FlutterThreadSynchronizer.mm", "framework/Source/FlutterTimeConverter.h", "framework/Source/FlutterTimeConverter.mm", "framework/Source/FlutterVSyncWaiter.h", @@ -190,10 +192,10 @@ executable("flutter_desktop_darwin_unittests") { "framework/Source/FlutterMutatorViewTest.mm", "framework/Source/FlutterPlatformNodeDelegateMacTest.mm", "framework/Source/FlutterPlatformViewControllerTest.mm", + "framework/Source/FlutterResizeSynchronizerTest.mm", "framework/Source/FlutterSurfaceManagerTest.mm", "framework/Source/FlutterTextInputPluginTest.mm", "framework/Source/FlutterTextInputSemanticsObjectTest.mm", - "framework/Source/FlutterThreadSynchronizerTest.mm", "framework/Source/FlutterVSyncWaiterTest.mm", "framework/Source/FlutterViewControllerTest.mm", "framework/Source/FlutterViewControllerTestUtils.h", diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 2670c0faf0b30..dfe72b94d5dc4 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -24,6 +24,7 @@ #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/FlutterRunLoop.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTimeConverter.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" @@ -37,6 +38,8 @@ using flutter::kFlutterImplicitViewId; +extern void UglyHackFlushMicrotasks(); + /** * Constructs and returns a FlutterLocale struct corresponding to |locale|, which must outlive * the returned struct. @@ -455,8 +458,6 @@ @implementation FlutterEngine { // A method channel for miscellaneous platform functionality. FlutterMethodChannel* _platformChannel; - FlutterThreadSynchronizer* _threadSynchronizer; - // Whether the application is currently the active application. BOOL _active; @@ -499,6 +500,9 @@ - (instancetype)initWithName:(NSString*)labelPrefix allowHeadlessExecution:(BOOL)allowHeadlessExecution { self = [super init]; NSAssert(self, @"Super init cannot be nil"); + + [FlutterRunLoop ensureMainLoopInitialized]; + _pasteboard = [[FlutterPasteboard alloc] init]; _active = NO; _visible = NO; @@ -527,7 +531,6 @@ - (instancetype)initWithName:(NSString*)labelPrefix object:nil]; _platformViewController = [[FlutterPlatformViewController alloc] init]; - _threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; [self setUpPlatformViewChannel]; [self setUpAccessibilityChannel]; [self setUpNotificationCenterListeners]; @@ -653,7 +656,8 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { const FlutterCustomTaskRunners custom_task_runners = { .struct_size = sizeof(FlutterCustomTaskRunners), .platform_task_runner = &cocoa_task_runner_description, - .thread_priority_setter = SetThreadPriority}; + .thread_priority_setter = SetThreadPriority, + .merged_ui_thread = 1}; flutterArguments.custom_task_runners = &custom_task_runners; [self loadAOTData:_project.assetsPath]; @@ -739,9 +743,7 @@ - (void)registerViewController:(FlutterViewController*)controller NSAssert([_viewControllers objectForKey:@(viewIdentifier)] == nil, @"The requested view ID is occupied."); [_viewControllers setObject:controller forKey:@(viewIdentifier)]; - [controller setUpWithEngine:self - viewIdentifier:viewIdentifier - threadSynchronizer:_threadSynchronizer]; + [controller setUpWithEngine:self viewIdentifier:viewIdentifier]; NSAssert(controller.viewIdentifier == viewIdentifier, @"Failed to assign view ID."); // Verify that the controller's property are updated accordingly. Failing the // assertions is likely because either the FlutterViewController or the @@ -769,13 +771,7 @@ - (void)viewControllerViewDidLoad:(FlutterViewController*)viewController { [timeConverter CAMediaTimeToEngineTime:targetTimestamp]; 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); - }]; + engine->_embedderAPI.OnVsync(_engine, baton, timeNanos, targetTimeNanos); } }]; FML_DCHECK([_vsyncWaiters objectForKey:@(viewController.viewIdentifier)] == nil); @@ -1132,9 +1128,6 @@ - (void)shutDownEngine { return; } - [_threadSynchronizer shutdown]; - _threadSynchronizer = nil; - FlutterEngineResult result = _embedderAPI.Deinitialize(_engine); if (result != kSuccess) { NSLog(@"Could not de-initialize the Flutter engine: error %d", result); @@ -1331,10 +1324,6 @@ - (BOOL)clipboardHasStrings { return flutter::GetSwitchesFromEnvironment(); } -- (FlutterThreadSynchronizer*)testThreadSynchronizer { - return _threadSynchronizer; -} - #pragma mark - FlutterAppLifecycleDelegate - (void)setApplicationState:(flutter::AppLifecycleState)state { @@ -1518,29 +1507,23 @@ - (BOOL)unregisterTextureWithID:(int64_t)textureID { #pragma mark - Task runner integration -- (void)runTaskOnEmbedder:(FlutterTask)task { - if (_engine) { - auto result = _embedderAPI.RunTask(_engine, &task); - if (result != kSuccess) { - NSLog(@"Could not post a task to the Flutter engine."); - } - } -} - - (void)postMainThreadTask:(FlutterTask)task targetTimeInNanoseconds:(uint64_t)targetTime { __weak FlutterEngine* weakSelf = self; - auto worker = ^{ - [weakSelf runTaskOnEmbedder:task]; - }; const auto engine_time = _embedderAPI.GetCurrentTime(); - if (targetTime <= engine_time) { - dispatch_async(dispatch_get_main_queue(), worker); - } else { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, targetTime - engine_time), - dispatch_get_main_queue(), worker); - } + [FlutterRunLoop.mainRunLoop + performBlock:^{ + FlutterEngine* self = weakSelf; + if (_engine) { + auto result = _embedderAPI.RunTask(_engine, &task); + if (result != kSuccess) { + NSLog(@"Could not post a task to the Flutter engine."); + } + } + UglyHackFlushMicrotasks(); + } + afterDelay:(targetTime - (double)engine_time) / 1000000000.0]; } // Getter used by test harness, only exposed through the FlutterEngine(Test) category diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index 8091fe873f7b9..f975b93501f79 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -33,8 +33,6 @@ // CREATE_NATIVE_ENTRY and MOCK_ENGINE_PROC are leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) -constexpr int64_t kImplicitViewId = 0ll; - @interface FlutterEngine (Test) /** * The FlutterCompositor object currently in use by the FlutterEngine. @@ -526,7 +524,7 @@ @implementation MockableFlutterEngine result:^(id result){ }]; - [engine.testThreadSynchronizer blockUntilFrameAvailable]; + // [engine.testThreadSynchronizer blockUntilFrameAvailable]; CALayer* rootLayer = viewController.flutterView.layer; @@ -863,20 +861,6 @@ @implementation MockableFlutterEngine } } -TEST_F(FlutterEngineTest, ThreadSynchronizerNotBlockingRasterThreadAfterShutdown) { - FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; - [threadSynchronizer shutdown]; - - std::thread rasterThread([&threadSynchronizer] { - [threadSynchronizer performCommitForView:kImplicitViewId - size:CGSizeMake(100, 100) - notify:^{ - }]; - }); - - rasterThread.join(); -} - TEST_F(FlutterEngineTest, ManageControllersIfInitiatedByController) { NSString* fixtures = @(flutter::testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index d1c328a2266bb..c7869a71c0695 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -224,10 +224,6 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { - (NSArray*)screens; @end -@interface FlutterEngine (Tests) -- (nonnull FlutterThreadSynchronizer*)testThreadSynchronizer; -@end - NS_ASSUME_NONNULL_END #endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERENGINE_INTERNAL_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h new file mode 100644 index 0000000000000..022aaf169b5d4 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRESIZESYNCHRONIZER_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRESIZESYNCHRONIZER_H_ + +#import + +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h" + +@interface FlutterResizeSynchronizer : NSObject + +/** + * Begins a resize operation for the given size. Block the thread until + * performCommitForSize: with the same size is called. + * While the thread is blocked Flutter messages are being pumped. + * See [FlutterRunLoop pollOnce]. + */ +- (void)beginResizeForSize:(CGSize)size notify:(nonnull dispatch_block_t)notify; + +/** + * Called from raster thread. Schedules the given block on platform thread + * at given delay and unblocks the platform thread if waiting for the surface + * during resize. + */ +- (void)performCommitForSize:(CGSize)size + notify:(nonnull dispatch_block_t)notify + delay:(NSTimeInterval)delay; + +/** + * Called when the view is shut down. Unblocks platform thread if blocked + * during resize. + */ +- (void)shutDown; + +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRESIZESYNCHRONIZER_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm new file mode 100644 index 0000000000000..cb62b7068d132 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm @@ -0,0 +1,60 @@ +// 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/FlutterResizeSynchronizer.h" + +#import "flutter/fml/logging.h" + +@implementation FlutterResizeSynchronizer { + std::atomic_bool _inResize; + bool _shuttingDown; + bool _hasFrame; + CGSize _contentSize; +} + +- (void)beginResizeForSize:(CGSize)size notify:(nonnull dispatch_block_t)notify { + if (!_hasFrame || _shuttingDown) { + notify(); + return; + } + + _inResize = true; + _contentSize = CGSizeMake(-1, -1); + notify(); + CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); + while (true) { + if (CGSizeEqualToSize(_contentSize, size) || _shuttingDown) { + break; + } + if (CFAbsoluteTimeGetCurrent() - start > 1.0) { + FML_LOG(ERROR) << "Resize timed out."; + break; + } + [FlutterRunLoop.mainRunLoop pollOnce]; + } + _inResize = false; +} + +- (void)performCommitForSize:(CGSize)size + notify:(nonnull dispatch_block_t)notify + delay:(NSTimeInterval)delay { + if (_inResize) { + delay = 0; + } + [FlutterRunLoop.mainRunLoop + performBlock:^{ + _hasFrame = YES; + _contentSize = size; + notify(); + } + afterDelay:delay]; +} + +- (void)shutDown { + [FlutterRunLoop.mainRunLoop performBlock:^{ + _shuttingDown = YES; + }]; +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizerTest.mm similarity index 99% rename from shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizerTest.mm rename to shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizerTest.mm index a9fafe947f562..2aab9caf88eb7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizerTest.mm @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#if 0 + #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h" #import "flutter/fml/synchronization/waitable_event.h" @@ -374,3 +376,5 @@ - (void)joinRender { [scaffold joinMain]; EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); } + +#endif \ No newline at end of file diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h b/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h new file mode 100644 index 0000000000000..f28ff1fc79351 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h @@ -0,0 +1,46 @@ +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRUNLOOP_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRUNLOOP_H_ + +#import + +/** + * Interface for scheduling tasks on the run loop. + * + * Main difference between using `FlutterRunLoop` to schedule tasks compared to + * `dispatch_async` or `[NSRunLoop performBlock:]` is that `FlutterRunLoop` + * schedules the task in both common run loop mode and a private run loop mode, + * which allows it to run in mode where it only processes Flutter messages + * (`[FlutterRunLoop pollOnce]`). + */ +@interface FlutterRunLoop : NSObject + +/** + * Ensures that the `FlutterRunLoop` for main thread is initialized. Only + * needs to be called once and must be called on the main thread. + */ ++ (void)ensureMainLoopInitialized; + +/** + * Returns the `FlutterRunLoop` for the main thread. + */ ++ (FlutterRunLoop*)mainRunLoop; + +/** + * Schedules a block to be executed on the main thread. + */ +- (void)performBlock:(void (^)(void))block; + +/** + * Schedules a block to be executed on the main thread after a delay. + */ +- (void)performBlock:(void (^)(void))block afterDelay:(NSTimeInterval)delay; + +/** + * Executes single iteration of the run loop in the mode where only Flutter + * messages are processed. + */ +- (void)pollOnce; + +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERRUNLOOP_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.mm b/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.mm new file mode 100644 index 0000000000000..13be9203122c1 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.mm @@ -0,0 +1,130 @@ +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h" +#include "fml/logging.h" + +#include + +namespace { +struct Task { + void (^block)(void); + CFAbsoluteTime target_time; +}; + +CFStringRef kFlutterRunLoopMode = CFSTR("FlutterRunLoopMode"); + +FlutterRunLoop* mainLoop; + +} // namespace + +@implementation FlutterRunLoop (Private) + +CFRunLoopRef _runLoop; +CFRunLoopSourceRef _source; +CFRunLoopTimerRef _timer; +std::vector _tasks; + +@end + +@implementation FlutterRunLoop + +static void Perform(void* info) { + FlutterRunLoop* runner = (__bridge FlutterRunLoop*)info; + [runner performExpiredTasks]; +} + +static void PerformTimer(CFRunLoopTimerRef timer, void* info) { + FlutterRunLoop* runner = (__bridge FlutterRunLoop*)info; + [runner performExpiredTasks]; +} + +- (instancetype)init { + if (self = [super init]) { + _runLoop = CFRunLoopGetCurrent(); + CFRunLoopSourceContext sourceContext = { + .info = (__bridge void*)self, + .perform = Perform, + }; + _source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext); + CFRunLoopAddSource(_runLoop, _source, kCFRunLoopCommonModes); + CFRunLoopAddSource(_runLoop, _source, kFlutterRunLoopMode); + + CFRunLoopTimerContext timerContext = { + .info = (__bridge void*)self, + }; + _timer = CFRunLoopTimerCreate(kCFAllocatorDefault, HUGE_VALF, HUGE_VALF, 0, 0, PerformTimer, + &timerContext); + CFRunLoopAddTimer(_runLoop, _timer, kCFRunLoopCommonModes); + CFRunLoopAddTimer(_runLoop, _timer, kFlutterRunLoopMode); + } + return self; +} + +- (void)dealloc { + CFRunLoopTimerInvalidate(_timer); + CFRunLoopRemoveTimer(_runLoop, _timer, kCFRunLoopCommonModes); + CFRunLoopRemoveTimer(_runLoop, _timer, kFlutterRunLoopMode); + CFRunLoopSourceInvalidate(_source); + CFRunLoopRemoveSource(_runLoop, _source, kCFRunLoopCommonModes); + CFRunLoopRemoveSource(_runLoop, _source, kFlutterRunLoopMode); +} + +- (void)rearmTimer { + CFAbsoluteTime nextFireTime = HUGE_VALF; + for (const auto& task : _tasks) { + nextFireTime = std::min(nextFireTime, task.target_time); + } + CFRunLoopTimerSetNextFireDate(_timer, nextFireTime); +} + +- (void)performExpiredTasks { + std::vector expiredTasks; + @synchronized(self) { + CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); + std::vector::iterator it = _tasks.begin(); + while (it != _tasks.end()) { + if (it->target_time <= now) { + expiredTasks.push_back(std::move(*it)); + it = _tasks.erase(it); + } else { + ++it; + } + } + [self rearmTimer]; + } + for (const auto& task : expiredTasks) { + task.block(); + } +} + +- (void)performBlock:(void (^)(void))block afterDelay:(NSTimeInterval)delay { + @synchronized(self) { + _tasks.push_back({block, CFAbsoluteTimeGetCurrent() + delay}); + if (delay > 0) { + [self rearmTimer]; + } else { + CFRunLoopSourceSignal(_source); + CFRunLoopWakeUp(_runLoop); + } + } +} + +- (void)performBlock:(void (^)(void))block { + [self performBlock:block afterDelay:0]; +} + ++ (void)ensureMainLoopInitialized { + FML_DCHECK([NSRunLoop currentRunLoop] == [NSRunLoop mainRunLoop]); + if (mainLoop == nil) { + mainLoop = [[FlutterRunLoop alloc] init]; + } +} + ++ (FlutterRunLoop*)mainRunLoop { + FML_DCHECK(mainLoop != nil); + return mainLoop; +} + +- (void)pollOnce { + CFRunLoopRunInMode(kFlutterRunLoopMode, 0.1, YES); +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h index d515741538bc0..888b41e181a1b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h @@ -27,11 +27,13 @@ @protocol FlutterSurfaceManagerDelegate /* - * Schedules the block on the platform thread and blocks until the block is executed. + * Schedules the block on the platform thread. * Provided `frameSize` is used to unblock the platform thread if it waits for * a certain frame size during resizing. */ -- (void)onPresent:(CGSize)frameSize withBlock:(nonnull dispatch_block_t)block; +- (void)onPresent:(CGSize)frameSize + withBlock:(nonnull dispatch_block_t)block + delay:(NSTimeInterval)delay; @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm index d87aed4c9e4ce..295b79a95f0f1 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm @@ -222,44 +222,32 @@ - (void)presentSurfaces:(NSArray*)surfaces [commandBuffer commit]; [commandBuffer waitUntilScheduled]; - dispatch_block_t presentBlock = ^{ - // Get the actual dimensions of the frame (relevant for thread synchronizer). - CGSize size = GetRequiredFrameSize(surfaces); - [_delegate onPresent:size - withBlock:^{ - _lastPresentationTime = presentationTime; - [self commit:surfaces]; - if (notify != nil) { - notify(); - } - }]; - }; + CGSize size = GetRequiredFrameSize(surfaces); + CFTimeInterval delay = 0; 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. - // - // This schedules a timer on current (raster) thread runloop. Raster thread at - // this point should be idle (the next frame vsync has not been signalled yet). - // - // Alternative could be simply blocking the raster thread, but that would show - // as a average_frame_rasterizer_time_millis regresson. CFTimeInterval minPresentationTime = (presentationTime + _lastPresentationTime) / 2.0; CFTimeInterval now = CACurrentMediaTime(); - if (now < minPresentationTime) { - NSTimer* timer = [NSTimer timerWithTimeInterval:minPresentationTime - now - repeats:NO - block:^(NSTimer* timer) { - presentBlock(); - }]; - [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; - return; - } + delay = std::max(minPresentationTime - now, 0.0); } - presentBlock(); + + [_delegate onPresent:size + withBlock:^{ + _lastPresentationTime = presentationTime; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self commit:surfaces]; + if (notify != nil) { + notify(); + } + [CATransaction commit]; + } + delay:delay]; } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm index 8d6996280e691..7ffb09da43119 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm @@ -1,7 +1,7 @@ // 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. - +#if 0 #import #import @@ -297,3 +297,5 @@ - (void)onPresent:(CGSize)frameSize withBlock:(nonnull dispatch_block_t)block { } } // namespace flutter::testing + +#endif diff --git a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h deleted file mode 100644 index a7a1020077e98..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h +++ /dev/null @@ -1,112 +0,0 @@ -// 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. - -#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERTHREADSYNCHRONIZER_H_ -#define FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERTHREADSYNCHRONIZER_H_ - -#import - -#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" - -/** - * Takes care of synchronization between raster and platform thread. - * - * All methods of this class must be called from the platform thread, - * except for performCommitForView:size:notify:. - */ -@interface FlutterThreadSynchronizer : NSObject - -/** - * Creates a FlutterThreadSynchronizer that uses the OS main thread as the - * platform thread. - */ -- (nullable instancetype)init; - -/** - * Blocks until all views have a commit with their given sizes (or empty) is requested. - */ -- (void)beginResizeForView:(FlutterViewIdentifier)viewIdentifier - size:(CGSize)size - notify:(nonnull dispatch_block_t)notify; - -/** - * Called from raster thread. Schedules the given block on platform thread - * and blocks until it is performed. - * - * If platform thread is blocked in `beginResize:` for given size (or size is empty), - * unblocks platform thread. - * - * The notify block is guaranteed to be called within a core animation transaction. - */ -- (void)performCommitForView:(FlutterViewIdentifier)viewIdentifier - 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. - * - * A view must be registered before calling begineResizeForView: or - * performCommitForView:. It is typically done when the view controller is - * created. - */ -- (void)registerView:(FlutterViewIdentifier)viewIdentifier; - -/** - * Requests the synchronizer to no longer track a view. - * - * It is typically done when the view controller is destroyed. - */ -- (void)deregisterView:(FlutterViewIdentifier)viewIdentifier; - -/** - * Called when the engine shuts down. - * - * Prevents any further synchronization and no longer blocks any threads. - */ -- (void)shutdown; - -@end - -@interface FlutterThreadSynchronizer (TestUtils) - -/** - * Creates a FlutterThreadSynchronizer that uses the specified queue as the - * platform thread. - */ -- (nullable instancetype)initWithMainQueue:(nonnull dispatch_queue_t)queue; - -/** - * Blocks current thread until the mutex is available, then return whether the - * synchronizer is waiting for a correct commit during resizing. - * - * After calling an operation of the thread synchronizer, call this method, - * and when it returns, the thread synchronizer can be at one of the following 3 - * states: - * - * 1. The operation has not started at all (with a return value FALSE.) - * 2. The operation has ended (with a return value FALSE.) - * 3. beginResizeForView: is in progress, waiting (with a return value TRUE.) - * - * By eliminating the 1st case (such as using the notify callback), we can use - * this return value to decide whether the synchronizer is in case 2 or case 3, - * that is whether the resizing is blocked by a mismatching commit. - */ -- (BOOL)isWaitingWhenMutexIsAvailable; - -/** - * Blocks current thread until there is frame available. - * Used in FlutterEngineTest. - */ -- (void)blockUntilFrameAvailable; - -@end - -#endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERTHREADSYNCHRONIZER_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm deleted file mode 100644 index b1de918a9aeb4..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm +++ /dev/null @@ -1,205 +0,0 @@ -// 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/FlutterThreadSynchronizer.h" - -#import - -#include -#include -#include - -#import "flutter/fml/logging.h" -#import "flutter/fml/synchronization/waitable_event.h" - -@interface FlutterThreadSynchronizer () { - dispatch_queue_t _mainQueue; - std::mutex _mutex; - BOOL _shuttingDown; - std::unordered_map _contentSizes; - std::vector _scheduledBlocks; - - BOOL _beginResizeWaiting; - - // Used to block [beginResize:]. - std::condition_variable _condBlockBeginResize; -} - -/** - * Returns true if all existing views have a non-zero size. - * - * If there are no views, still returns true. - */ -- (BOOL)allViewsHaveFrame; - -/** - * Returns true if there are any views that have a non-zero size. - * - * If there are no views, returns false. - */ -- (BOOL)someViewsHaveFrame; - -@end - -@implementation FlutterThreadSynchronizer - -- (instancetype)init { - return [self initWithMainQueue:dispatch_get_main_queue()]; -} - -- (instancetype)initWithMainQueue:(dispatch_queue_t)queue { - self = [super init]; - if (self != nil) { - _mainQueue = queue; - } - return self; -} - -- (BOOL)allViewsHaveFrame { - for (auto const& [viewIdentifier, contentSize] : _contentSizes) { - if (CGSizeEqualToSize(contentSize, CGSizeZero)) { - return NO; - } - } - return YES; -} - -- (BOOL)someViewsHaveFrame { - for (auto const& [viewIdentifier, contentSize] : _contentSizes) { - if (!CGSizeEqualToSize(contentSize, CGSizeZero)) { - return YES; - } - } - return NO; -} - -- (void)drain { - dispatch_assert_queue(_mainQueue); - - [CATransaction begin]; - [CATransaction setDisableActions:YES]; - for (dispatch_block_t block : _scheduledBlocks) { - block(); - } - [CATransaction commit]; - _scheduledBlocks.clear(); -} - -- (void)blockUntilFrameAvailable { - std::unique_lock lock(_mutex); - [self drain]; - - _beginResizeWaiting = YES; - while (![self someViewsHaveFrame] && !_shuttingDown) { - _condBlockBeginResize.wait(lock); - [self drain]; - } - - _beginResizeWaiting = NO; -} - -- (void)beginResizeForView:(FlutterViewIdentifier)viewIdentifier - size:(CGSize)size - notify:(nonnull dispatch_block_t)notify { - dispatch_assert_queue(_mainQueue); - std::unique_lock lock(_mutex); - - if (![self allViewsHaveFrame] || _shuttingDown) { - // No blocking until framework produces at least one frame - notify(); - return; - } - - [self drain]; - - notify(); - - _contentSizes[viewIdentifier] = CGSizeMake(-1, -1); - - _beginResizeWaiting = YES; - - while (true) { - if (_shuttingDown) { - break; - } - const CGSize& contentSize = _contentSizes[viewIdentifier]; - if (CGSizeEqualToSize(contentSize, size) || CGSizeEqualToSize(contentSize, CGSizeZero)) { - break; - } - _condBlockBeginResize.wait(lock); - [self drain]; - } - - _beginResizeWaiting = NO; -} - -- (void)performCommitForView:(FlutterViewIdentifier)viewIdentifier - size:(CGSize)size - notify:(nonnull dispatch_block_t)notify { - dispatch_assert_queue_not(_mainQueue); - fml::AutoResetWaitableEvent event; - { - std::unique_lock lock(_mutex); - if (_shuttingDown) { - // Engine is shutting down, main thread may be blocked by the engine - // waiting for raster thread to finish. - return; - } - fml::AutoResetWaitableEvent& e = event; - _scheduledBlocks.push_back(^{ - notify(); - _contentSizes[viewIdentifier] = size; - e.Signal(); - }); - if (_beginResizeWaiting) { - _condBlockBeginResize.notify_all(); - } else { - dispatch_async(_mainQueue, ^{ - std::unique_lock lock(_mutex); - [self drain]; - }); - } - } - event.Wait(); -} - -- (void)performOnPlatformThread:(nonnull dispatch_block_t)block { - std::unique_lock lock(_mutex); - _scheduledBlocks.push_back(block); - if (_beginResizeWaiting) { - _condBlockBeginResize.notify_all(); - } else { - dispatch_async(_mainQueue, ^{ - std::unique_lock lock(_mutex); - [self drain]; - }); - } -} - -- (void)registerView:(FlutterViewIdentifier)viewIdentifier { - dispatch_assert_queue(_mainQueue); - std::unique_lock lock(_mutex); - _contentSizes[viewIdentifier] = CGSizeZero; -} - -- (void)deregisterView:(FlutterViewIdentifier)viewIdentifier { - dispatch_assert_queue(_mainQueue); - std::unique_lock lock(_mutex); - _contentSizes.erase(viewIdentifier); -} - -- (void)shutdown { - dispatch_assert_queue(_mainQueue); - std::unique_lock lock(_mutex); - _shuttingDown = YES; - _condBlockBeginResize.notify_all(); - [self drain]; -} - -- (BOOL)isWaitingWhenMutexIsAvailable { - std::unique_lock lock(_mutex); - return _beginResizeWaiting; -} - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h b/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h index 2bb574f775fc5..faa6d86eb6d29 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h @@ -10,15 +10,13 @@ /// 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:. +/// Provided |block| will be invoked on platform thread. - (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 diff --git a/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm b/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm index eb4d6fb9b3558..e876201266a00 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.mm @@ -1,5 +1,6 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterVSyncWaiter.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDisplayLink.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRunLoop.h" #include "flutter/fml/logging.h" @@ -37,7 +38,6 @@ @implementation FlutterVSyncWaiter { std::optional _pendingBaton; FlutterDisplayLink* _displayLink; void (^_block)(CFTimeInterval, CFTimeInterval, uintptr_t); - NSRunLoop* _runLoop; CFTimeInterval _lastTargetTimestamp; BOOL _warmUpFrame; } @@ -59,48 +59,37 @@ - (instancetype)initWithDisplayLink:(FlutterDisplayLink*)displayLink 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", _pendingBaton.value_or(0)); - - NSTimer* timer = [NSTimer - timerWithTimeInterval:remaining - repeats:NO - block:^(NSTimer* _Nonnull timer) { - if (!_pendingBaton.has_value()) { - TRACE_VSYNC("DisplayLinkPaused", size_t(0)); - _displayLink.paused = YES; - return; - } - TRACE_VSYNC("DisplayLinkCallback-Delayed", _pendingBaton.value_or(0)); - _block(minStart, targetTimestamp, *_pendingBaton); - _pendingBaton = std::nullopt; - }]; - [_runLoop addTimer:timer forMode:NSRunLoopCommonModes]; -} - // Called from display link thread. - (void)onDisplayLink:(CFTimeInterval)timestamp targetTimestamp:(CFTimeInterval)targetTimestamp { @synchronized(self) { - if (_runLoop == nil) { + if (_lastTargetTimestamp == 0) { // Initial vsync - timestamp will be used to determine vsync phase. _lastTargetTimestamp = targetTimestamp; _displayLink.paused = YES; } else { - [_runLoop performBlock:^{ - [self processDisplayLink:timestamp targetTimestamp:targetTimestamp]; + _lastTargetTimestamp = targetTimestamp; + [FlutterRunLoop.mainRunLoop performBlock:^{ + // 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", _pendingBaton.value_or(0)); + + [FlutterRunLoop.mainRunLoop + performBlock:^{ + if (!_pendingBaton.has_value()) { + TRACE_VSYNC("DisplayLinkPaused", size_t(0)); + _displayLink.paused = YES; + return; + } + TRACE_VSYNC("DisplayLinkCallback-Delayed", _pendingBaton.value_or(0)); + _block(minStart, targetTimestamp, *_pendingBaton); + _pendingBaton = std::nullopt; + } + afterDelay:remaining]; }]; } } @@ -120,14 +109,6 @@ - (void)waitForVSync:(uintptr_t)baton { return; } - // 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 (_pendingBaton.has_value()) { FML_LOG(WARNING) << "Engine requested vsync while another was pending"; _block(0, 0, *_pendingBaton); @@ -161,15 +142,13 @@ - (void)waitForVSync:(uintptr_t)baton { 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]; + [FlutterRunLoop.mainRunLoop + performBlock:^{ + CFTimeInterval targetTimestamp = start + tick_interval; + TRACE_VSYNC("SynthesizedInitialVSync", baton); + _block(start, targetTimestamp, baton); + } + afterDelay:delay]; _displayLink.paused = NO; } else { _pendingBaton = baton; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index e22e50f6af580..d39d0bb821210 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -7,8 +7,8 @@ #import +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h" #include @@ -40,7 +40,6 @@ - (nullable instancetype)initWithMTLDevice:(nonnull id)device commandQueue:(nonnull id)commandQueue delegate:(nonnull id)delegate - threadSynchronizer:(nonnull FlutterThreadSynchronizer*)threadSynchronizer viewIdentifier:(FlutterViewIdentifier)viewIdentifier NS_DESIGNATED_INITIALIZER; @@ -72,6 +71,8 @@ */ - (void)didUpdateMouseCursor:(nonnull NSCursor*)cursor; +- (void)shutDown; + @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_MACOS_FRAMEWORK_SOURCE_FLUTTERVIEW_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/shell/platform/darwin/macos/framework/Source/FlutterView.mm index 21a1115e73d73..ff365ea008b76 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -4,16 +4,16 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h" #import @interface FlutterView () { FlutterViewIdentifier _viewIdentifier; __weak id _viewDelegate; - FlutterThreadSynchronizer* _threadSynchronizer; FlutterSurfaceManager* _surfaceManager; + FlutterResizeSynchronizer* _resizeSynchronizer; NSCursor* _lastCursor; } @@ -24,7 +24,6 @@ @implementation FlutterView - (instancetype)initWithMTLDevice:(id)device commandQueue:(id)commandQueue delegate:(id)delegate - threadSynchronizer:(FlutterThreadSynchronizer*)threadSynchronizer viewIdentifier:(FlutterViewIdentifier)viewIdentifier { self = [super initWithFrame:NSZeroRect]; if (self) { @@ -33,30 +32,25 @@ - (instancetype)initWithMTLDevice:(id)device [self setLayerContentsRedrawPolicy:NSViewLayerContentsRedrawDuringViewResize]; _viewIdentifier = viewIdentifier; _viewDelegate = delegate; - _threadSynchronizer = threadSynchronizer; _surfaceManager = [[FlutterSurfaceManager alloc] initWithDevice:device commandQueue:commandQueue layer:self.layer delegate:self]; + _resizeSynchronizer = [[FlutterResizeSynchronizer alloc] init]; } return self; } -- (void)onPresent:(CGSize)frameSize withBlock:(dispatch_block_t)block { - [_threadSynchronizer performCommitForView:_viewIdentifier size:frameSize notify:block]; +- (void)onPresent:(CGSize)frameSize withBlock:(dispatch_block_t)block delay:(NSTimeInterval)delay { + [_resizeSynchronizer performCommitForSize:frameSize notify:block delay:delay]; } -- (FlutterSurfaceManager*)surfaceManager { - return _surfaceManager; +- (void)shutDown { + [_resizeSynchronizer shutDown]; } -- (void)reshaped { - CGSize scaledSize = [self convertSizeToBacking:self.bounds.size]; - [_threadSynchronizer beginResizeForView:_viewIdentifier - size:scaledSize - notify:^{ - [_viewDelegate viewDidReshape:self]; - }]; +- (FlutterSurfaceManager*)surfaceManager { + return _surfaceManager; } - (void)setBackgroundColor:(NSColor*)color { @@ -67,7 +61,11 @@ - (void)setBackgroundColor:(NSColor*)color { - (void)setFrameSize:(NSSize)newSize { [super setFrameSize:newSize]; - [self reshaped]; + CGSize scaledSize = [self convertSizeToBacking:self.bounds.size]; + [_resizeSynchronizer beginResizeForSize:scaledSize + notify:^{ + [_viewDelegate viewDidReshape:self]; + }]; } /** diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index a091a48a56945..7dff5168fdfa9 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -323,10 +323,6 @@ @implementation FlutterViewController { FlutterDartProject* _project; std::shared_ptr _bridge; - - // FlutterViewController does not actually uses the synchronizer, but only - // passes it to FlutterView. - FlutterThreadSynchronizer* _threadSynchronizer; } @synthesize viewIdentifier = _viewIdentifier; @@ -498,19 +494,15 @@ - (void)notifySemanticsEnabledChanged { } - (void)setUpWithEngine:(FlutterEngine*)engine - viewIdentifier:(FlutterViewIdentifier)viewIdentifier - threadSynchronizer:(FlutterThreadSynchronizer*)threadSynchronizer { + viewIdentifier:(FlutterViewIdentifier)viewIdentifier { NSAssert(_engine == nil, @"Already attached to an engine %@.", _engine); _engine = engine; _viewIdentifier = viewIdentifier; - _threadSynchronizer = threadSynchronizer; - [_threadSynchronizer registerView:_viewIdentifier]; } - (void)detachFromEngine { NSAssert(_engine != nil, @"Not attached to any engine."); - [_threadSynchronizer deregisterView:_viewIdentifier]; - _threadSynchronizer = nil; + [self.flutterView shutDown]; _engine = nil; } @@ -832,7 +824,6 @@ - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id)device return [[FlutterView alloc] initWithMTLDevice:device commandQueue:commandQueue delegate:self - threadSynchronizer:_threadSynchronizer viewIdentifier:_viewIdentifier]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index 4e35c0288b346..5533d2182dff3 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -38,8 +38,7 @@ * before being used, and must be set up only once until detachFromEngine:. */ - (void)setUpWithEngine:(nonnull FlutterEngine*)engine - viewIdentifier:(FlutterViewIdentifier)viewIdentifier - threadSynchronizer:(nonnull FlutterThreadSynchronizer*)threadSynchronizer; + viewIdentifier:(FlutterViewIdentifier)viewIdentifier; /** * Reset the `engine` and `id` of this controller. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm index 3c84116188392..d64c6d1603622 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm @@ -29,11 +29,9 @@ - (BOOL)viewShouldAcceptFirstResponder:(NSView*)view { id device = MTLCreateSystemDefaultDevice(); id queue = [device newCommandQueue]; TestFlutterViewDelegate* delegate = [[TestFlutterViewDelegate alloc] init]; - FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; FlutterView* view = [[FlutterView alloc] initWithMTLDevice:device commandQueue:queue delegate:delegate - threadSynchronizer:threadSynchronizer viewIdentifier:kImplicitViewId]; EXPECT_EQ([view layer:view.layer shouldInheritContentsScale:3.0 fromWindow:view.window], YES); } @@ -74,11 +72,9 @@ - (void)set { id device = MTLCreateSystemDefaultDevice(); id queue = [device newCommandQueue]; TestFlutterViewDelegate* delegate = [[TestFlutterViewDelegate alloc] init]; - FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; TestFlutterView* view = [[TestFlutterView alloc] initWithMTLDevice:device commandQueue:queue delegate:delegate - threadSynchronizer:threadSynchronizer viewIdentifier:kImplicitViewId]; NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask @@ -118,11 +114,9 @@ - (void)set { id device = MTLCreateSystemDefaultDevice(); id queue = [device newCommandQueue]; TestFlutterViewDelegate* delegate = [[TestFlutterViewDelegate alloc] init]; - FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; TestFlutterView* view = [[TestFlutterView alloc] initWithMTLDevice:device commandQueue:queue delegate:delegate - threadSynchronizer:threadSynchronizer viewIdentifier:kImplicitViewId]; NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 11e40b8a40ba0..60477f3ac31a5 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -1607,6 +1607,10 @@ typedef struct { /// Specify a callback that is used to set the thread priority for embedder /// task runners. void (*thread_priority_setter)(FlutterThreadPriority); + + /// Whether the UI thread should be merged with platform thread. If true, the + /// platform_task_runner will be used to schedule UI thread tasks. + int merged_ui_thread; } FlutterCustomTaskRunners; typedef struct { diff --git a/shell/platform/embedder/embedder_thread_host.cc b/shell/platform/embedder/embedder_thread_host.cc index 7cca136462ed5..0ccc99a973162 100644 --- a/shell/platform/embedder/embedder_thread_host.cc +++ b/shell/platform/embedder/embedder_thread_host.cc @@ -130,14 +130,18 @@ EmbedderThreadHost::CreateEmbedderManagedThreadHost( return nullptr; } + int merged_ui_thread = SAFE_ACCESS(custom_task_runners, merged_ui_thread, 0); + auto thread_host_config = ThreadHost::ThreadHostConfig(config_setter); // The UI and IO threads are always created by the engine and the embedder has // no opportunity to specify task runners for the same. // // If/when more task runners are exposed, this mask will need to be updated. - thread_host_config.SetUIConfig(MakeThreadConfig( - ThreadHost::Type::kUi, fml::Thread::ThreadPriority::kDisplay)); + if (!merged_ui_thread) { + thread_host_config.SetUIConfig(MakeThreadConfig( + ThreadHost::Type::kUi, fml::Thread::ThreadPriority::kDisplay)); + } thread_host_config.SetIOConfig(MakeThreadConfig( ThreadHost::Type::kIo, fml::Thread::ThreadPriority::kBackground)); @@ -187,12 +191,16 @@ EmbedderThreadHost::CreateEmbedderManagedThreadHost( render_task_runner_pair.second) : thread_host.raster_thread->GetTaskRunner(); + auto ui_task_runner = merged_ui_thread + ? platform_task_runner + : thread_host.ui_thread->GetTaskRunner(); + flutter::TaskRunners task_runners( kFlutterThreadName, - platform_task_runner, // platform - render_task_runner, // raster - thread_host.ui_thread->GetTaskRunner(), // ui (always engine managed) - thread_host.io_thread->GetTaskRunner() // io (always engine managed) + platform_task_runner, // platform + render_task_runner, // raster + ui_task_runner, + thread_host.io_thread->GetTaskRunner() // io (always engine managed) ); if (!task_runners.IsValid()) {