diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 0158c116e1316..bcd91c598a7da 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2744,6 +2744,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/FlutterUmbrellaImportTests.m + ../../../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 @@ -5410,6 +5413,7 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextu FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h 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/FlutterView.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index edebb4ebfd3b1..16ffb24cd3f19 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -182,6 +182,7 @@ executable("flutter_desktop_darwin_unittests") { "framework/Source/FlutterSurfaceManagerTest.mm", "framework/Source/FlutterTextInputPluginTest.mm", "framework/Source/FlutterTextInputSemanticsObjectTest.mm", + "framework/Source/FlutterThreadSynchronizerTest.mm", "framework/Source/FlutterViewControllerTest.mm", "framework/Source/FlutterViewControllerTestUtils.h", "framework/Source/FlutterViewControllerTestUtils.mm", diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index c456e42449211..d4f194cfca01e 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -388,6 +388,8 @@ @implementation FlutterEngine { // A method channel for miscellaneous platform functionality. FlutterMethodChannel* _platformChannel; + FlutterThreadSynchronizer* _threadSynchronizer; + int _nextViewId; } @@ -427,6 +429,7 @@ - (instancetype)initWithName:(NSString*)labelPrefix object:nil]; _platformViewController = [[FlutterPlatformViewController alloc] init]; + _threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; [self setUpPlatformViewChannel]; [self setUpAccessibilityChannel]; [self setUpNotificationCenterListeners]; @@ -589,7 +592,7 @@ - (void)registerViewController:(FlutterViewController*)controller forId:(Flutter NSAssert(![controller attached], @"The incoming view controller is already attached to an engine."); NSAssert([_viewControllers objectForKey:@(viewId)] == nil, @"The requested view ID is occupied."); - [controller attachToEngine:self withId:viewId]; + [controller setUpWithEngine:self viewId:viewId threadSynchronizer:_threadSynchronizer]; NSAssert(controller.viewId == viewId, @"Failed to assign view ID."); [_viewControllers setObject:controller forKey:@(viewId)]; } @@ -928,11 +931,8 @@ - (void)shutDownEngine { return; } - NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; - FlutterViewController* nextViewController; - while ((nextViewController = [viewControllerEnumerator nextObject])) { - [nextViewController.flutterView shutdown]; - } + [_threadSynchronizer shutdown]; + _threadSynchronizer = nil; FlutterEngineResult result = _embedderAPI.Deinitialize(_engine); if (result != kSuccess) { @@ -1117,6 +1117,10 @@ - (NSPasteboard*)pasteboard { return flutter::GetSwitchesFromEnvironment(); } +- (FlutterThreadSynchronizer*)testThreadSynchronizer { + return _threadSynchronizer; +} + #pragma mark - FlutterBinaryMessenger - (void)sendOnChannel:(nonnull NSString*)channel message:(nullable NSData*)message { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index 1d9afad90de92..cf81a8ad798c2 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -25,6 +25,8 @@ // CREATE_NATIVE_ENTRY and MOCK_ENGINE_PROC are leaky by design // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) +constexpr int64_t kDefaultViewId = 0ll; + @interface FlutterEngine (Test) /** * The FlutterCompositor object currently in use by the FlutterEngine. @@ -32,6 +34,7 @@ @interface FlutterEngine (Test) * May be nil if the compositor has not been initialized yet. */ @property(nonatomic, readonly, nullable) flutter::FlutterCompositor* macOSCompositor; + @end @interface TestPlatformViewFactory : NSObject @@ -438,7 +441,7 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable result:^(id result){ }]; - [viewController.flutterView.threadSynchronizer blockUntilFrameAvailable]; + [engine.testThreadSynchronizer blockUntilFrameAvailable]; CALayer* rootLayer = viewController.flutterView.layer; @@ -629,9 +632,10 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable [threadSynchronizer shutdown]; std::thread rasterThread([&threadSynchronizer] { - [threadSynchronizer performCommit:CGSizeMake(100, 100) - notify:^{ - }]; + [threadSynchronizer performCommitForView:kDefaultViewId + size:CGSizeMake(100, 100) + notify:^{ + }]; }); rasterThread.join(); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index 31c076dc1785f..be973222ca95d 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -195,4 +195,8 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { @end +@interface FlutterEngine (Tests) +- (nonnull FlutterThreadSynchronizer*)testThreadSynchronizer; +@end + NS_ASSUME_NONNULL_END diff --git a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h index 8eca9611f0f48..8d8d248bdefdb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h @@ -1,21 +1,29 @@ +// 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 /** * 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 /** - * Blocks current thread until there is frame available. - * Used in FlutterEngineTest. + * Creates a FlutterThreadSynchronizer that uses the OS main thread as the + * platform thread. */ -- (void)blockUntilFrameAvailable; +- (nullable instancetype)init; /** - * Called from platform thread. Blocks until commit with given size (or empty) - * is requested. + * Blocks until all views have a commit with their given sizes (or empty) is requested. */ -- (void)beginResize:(CGSize)size notify:(nonnull dispatch_block_t)notify; +- (void)beginResizeForView:(int64_t)viewId + size:(CGSize)size + notify:(nonnull dispatch_block_t)notify; /** * Called from raster thread. Schedules the given block on platform thread @@ -26,11 +34,65 @@ * * The notify block is guaranteed to be called within a core animation transaction. */ -- (void)performCommit:(CGSize)size notify:(nonnull dispatch_block_t)notify; +- (void)performCommitForView:(int64_t)viewId + size:(CGSize)size + notify:(nonnull dispatch_block_t)notify; + +/** + * 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:(int64_t)viewId; + +/** + * Requests the synchronizer to no longer track a view. + * + * It is typically done when the view controller is destroyed. + */ +- (void)deregisterView:(int64_t)viewId; /** - * Called when shutting down. Unblocks everything and prevents any further synchronization. + * 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 diff --git a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm index cb442ea768ffb..75a4ff5037210 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm @@ -1,17 +1,23 @@ +// 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; - CGSize _contentSize; + std::unordered_map _contentSizes; std::vector _scheduledBlocks; BOOL _beginResizeWaiting; @@ -20,12 +26,56 @@ @interface FlutterThreadSynchronizer () { 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& [viewId, contentSize] : _contentSizes) { + if (CGSizeEqualToSize(contentSize, CGSizeZero)) { + return NO; + } + } + return YES; +} + +- (BOOL)someViewsHaveFrame { + for (auto const& [viewId, contentSize] : _contentSizes) { + if (!CGSizeEqualToSize(contentSize, CGSizeZero)) { + return YES; + } + } + return NO; +} + - (void)drain { - FML_DCHECK([NSThread isMainThread]); + dispatch_assert_queue(_mainQueue); [CATransaction begin]; [CATransaction setDisableActions:YES]; @@ -41,7 +91,7 @@ - (void)blockUntilFrameAvailable { _beginResizeWaiting = YES; - while (CGSizeEqualToSize(_contentSize, CGSizeZero) && !_shuttingDown) { + while (![self someViewsHaveFrame] && !_shuttingDown) { _condBlockBeginResize.wait(lock); [self drain]; } @@ -49,10 +99,13 @@ - (void)blockUntilFrameAvailable { _beginResizeWaiting = NO; } -- (void)beginResize:(CGSize)size notify:(nonnull dispatch_block_t)notify { +- (void)beginResizeForView:(int64_t)viewId + size:(CGSize)size + notify:(nonnull dispatch_block_t)notify { + dispatch_assert_queue(_mainQueue); std::unique_lock lock(_mutex); - if (CGSizeEqualToSize(_contentSize, CGSizeZero) || _shuttingDown) { + if (![self allViewsHaveFrame] || _shuttingDown) { // No blocking until framework produces at least one frame notify(); return; @@ -62,12 +115,18 @@ - (void)beginResize:(CGSize)size notify:(nonnull dispatch_block_t)notify { notify(); - _contentSize = CGSizeMake(-1, -1); + _contentSizes[viewId] = CGSizeMake(-1, -1); _beginResizeWaiting = YES; - while (!CGSizeEqualToSize(_contentSize, size) && // - !CGSizeEqualToSize(_contentSize, CGSizeZero) && !_shuttingDown) { + while (true) { + if (_shuttingDown) { + break; + } + const CGSize& contentSize = _contentSizes[viewId]; + if (CGSizeEqualToSize(contentSize, size) || CGSizeEqualToSize(contentSize, CGSizeZero)) { + break; + } _condBlockBeginResize.wait(lock); [self drain]; } @@ -75,7 +134,10 @@ - (void)beginResize:(CGSize)size notify:(nonnull dispatch_block_t)notify { _beginResizeWaiting = NO; } -- (void)performCommit:(CGSize)size notify:(nonnull dispatch_block_t)notify { +- (void)performCommitForView:(int64_t)viewId + size:(CGSize)size + notify:(nonnull dispatch_block_t)notify { + dispatch_assert_queue_not(_mainQueue); fml::AutoResetWaitableEvent event; { std::unique_lock lock(_mutex); @@ -87,13 +149,13 @@ - (void)performCommit:(CGSize)size notify:(nonnull dispatch_block_t)notify { fml::AutoResetWaitableEvent& e = event; _scheduledBlocks.push_back(^{ notify(); - _contentSize = size; + _contentSizes[viewId] = size; e.Signal(); }); if (_beginResizeWaiting) { _condBlockBeginResize.notify_all(); } else { - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(_mainQueue, ^{ std::unique_lock lock(_mutex); [self drain]; }); @@ -102,11 +164,29 @@ - (void)performCommit:(CGSize)size notify:(nonnull dispatch_block_t)notify { event.Wait(); } +- (void)registerView:(int64_t)viewId { + dispatch_assert_queue(_mainQueue); + std::unique_lock lock(_mutex); + _contentSizes[viewId] = CGSizeZero; +} + +- (void)deregisterView:(int64_t)viewId { + dispatch_assert_queue(_mainQueue); + std::unique_lock lock(_mutex); + _contentSizes.erase(viewId); +} + - (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/FlutterThreadSynchronizerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizerTest.mm new file mode 100644 index 0000000000000..2541f8675576d --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizerTest.mm @@ -0,0 +1,383 @@ +// 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 +#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; + +- (nullable instancetype)init; +- (void)dispatchMainTask:(nonnull void (^)())task; +- (void)dispatchRenderTask:(nonnull void (^)())task; +- (void)joinMain; +- (void)joinRender; +@end + +@implementation FlutterThreadSynchronizerTestScaffold { + dispatch_queue_t _mainQueue; + std::shared_ptr _mainLatch; + + dispatch_queue_t _renderQueue; + std::shared_ptr _renderLatch; + + FlutterThreadSynchronizer* _synchronizer; +} + +@synthesize synchronizer = _synchronizer; + +- (nullable instancetype)init { + self = [super init]; + if (self != nil) { + _mainQueue = dispatch_queue_create("MAIN", DISPATCH_QUEUE_SERIAL); + _renderQueue = dispatch_queue_create("RENDER", DISPATCH_QUEUE_SERIAL); + _synchronizer = [[FlutterThreadSynchronizer alloc] initWithMainQueue:_mainQueue]; + } + return self; +} + +- (void)dispatchMainTask:(nonnull void (^)())task { + dispatch_async(_mainQueue, task); +} + +- (void)dispatchRenderTask:(nonnull void (^)())task { + dispatch_async(_renderQueue, task); +} + +- (void)joinMain { + fml::AutoResetWaitableEvent latch; + fml::AutoResetWaitableEvent* pLatch = &latch; + dispatch_async(_mainQueue, ^{ + pLatch->Signal(); + }); + latch.Wait(); +} + +- (void)joinRender { + fml::AutoResetWaitableEvent latch; + fml::AutoResetWaitableEvent* pLatch = &latch; + dispatch_async(_renderQueue, ^{ + pLatch->Signal(); + }); + latch.Wait(); +} + +@end + +TEST(FlutterThreadSynchronizerTest, RegularCommit) { + FlutterThreadSynchronizerTestScaffold* scaffold = + [[FlutterThreadSynchronizerTestScaffold alloc] init]; + FlutterThreadSynchronizer* synchronizer = scaffold.synchronizer; + + // Initial resize: does not block until the first frame. + __block int notifiedResize = 0; + [scaffold dispatchMainTask:^{ + [synchronizer registerView:1]; + [synchronizer beginResizeForView:1 + size:CGSize{5, 5} + notify:^{ + notifiedResize += 1; + }]; + }]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + [scaffold joinMain]; + EXPECT_EQ(notifiedResize, 1); + + // Still does not block. + [scaffold dispatchMainTask:^{ + [synchronizer beginResizeForView:1 + size:CGSize{7, 7} + notify:^{ + notifiedResize += 1; + }]; + }]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + [scaffold joinMain]; + EXPECT_EQ(notifiedResize, 2); + + // First frame + __block int notifiedCommit = 0; + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{7, 7} + notify:^{ + notifiedCommit += 1; + }]; + }]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + [scaffold joinRender]; + EXPECT_EQ(notifiedCommit, 1); +} + +TEST(FlutterThreadSynchronizerTest, ResizingBlocksRenderingUntilSizeMatches) { + FlutterThreadSynchronizerTestScaffold* scaffold = + [[FlutterThreadSynchronizerTestScaffold alloc] init]; + FlutterThreadSynchronizer* synchronizer = scaffold.synchronizer; + // A latch to ensure that a beginResizeForView: call has at least executed + // something, so that the isWaitingWhenMutexIsAvailable: call correctly stops + // at either when beginResizeForView: finishes or waits half way. + fml::AutoResetWaitableEvent begunResizingLatch; + fml::AutoResetWaitableEvent* begunResizing = &begunResizingLatch; + + // Initial resize: does not block until the first frame. + [scaffold dispatchMainTask:^{ + [synchronizer registerView:1]; + [synchronizer beginResizeForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + }]; + [scaffold joinMain]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + + // First frame. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Resize to (7, 7): blocks until the next frame. + [scaffold dispatchMainTask:^{ + [synchronizer beginResizeForView:1 + size:CGSize{7, 7} + notify:^{ + begunResizing->Signal(); + }]; + }]; + begunResizing->Wait(); + EXPECT_TRUE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Render with old size. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_TRUE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Render with new size. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{7, 7} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + + [scaffold joinMain]; +} + +TEST(FlutterThreadSynchronizerTest, ShutdownMakesEverythingNonBlocking) { + FlutterThreadSynchronizerTestScaffold* scaffold = + [[FlutterThreadSynchronizerTestScaffold alloc] init]; + FlutterThreadSynchronizer* synchronizer = scaffold.synchronizer; + fml::AutoResetWaitableEvent begunResizingLatch; + fml::AutoResetWaitableEvent* begunResizing = &begunResizingLatch; + + // Initial resize + [scaffold dispatchMainTask:^{ + [synchronizer registerView:1]; + [synchronizer beginResizeForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + }]; + [scaffold joinMain]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Push a frame. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + + [scaffold dispatchMainTask:^{ + [synchronizer shutdown]; + }]; + + // Resize to (7, 7). Should not block any frames since it has shut down. + [scaffold dispatchMainTask:^{ + [synchronizer beginResizeForView:1 + size:CGSize{7, 7} + notify:^{ + begunResizing->Signal(); + }]; + }]; + begunResizing->Wait(); + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + [scaffold joinMain]; + + // All further calls should be unblocking. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{9, 9} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); +} + +TEST(FlutterThreadSynchronizerTest, RegularCommitForMultipleViews) { + FlutterThreadSynchronizerTestScaffold* scaffold = + [[FlutterThreadSynchronizerTestScaffold alloc] init]; + FlutterThreadSynchronizer* synchronizer = scaffold.synchronizer; + fml::AutoResetWaitableEvent begunResizingLatch; + fml::AutoResetWaitableEvent* begunResizing = &begunResizingLatch; + + // Initial resize: does not block until the first frame. + [scaffold dispatchMainTask:^{ + [synchronizer registerView:1]; + [synchronizer registerView:2]; + [synchronizer beginResizeForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + [synchronizer beginResizeForView:2 + size:CGSize{15, 15} + notify:^{ + begunResizing->Signal(); + }]; + }]; + begunResizing->Wait(); + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + [scaffold joinMain]; + + // Still does not block. + [scaffold dispatchMainTask:^{ + [synchronizer beginResizeForView:1 + size:CGSize{7, 7} + notify:^{ + begunResizing->Signal(); + }]; + }]; + begunResizing->Signal(); + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + [scaffold joinMain]; + + // First frame + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{7, 7} + notify:^{ + }]; + [synchronizer performCommitForView:2 + size:CGSize{15, 15} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); +} + +TEST(FlutterThreadSynchronizerTest, ResizingForMultipleViews) { + FlutterThreadSynchronizerTestScaffold* scaffold = + [[FlutterThreadSynchronizerTestScaffold alloc] init]; + FlutterThreadSynchronizer* synchronizer = scaffold.synchronizer; + fml::AutoResetWaitableEvent begunResizingLatch; + fml::AutoResetWaitableEvent* begunResizing = &begunResizingLatch; + + // Initial resize: does not block until the first frame. + [scaffold dispatchMainTask:^{ + [synchronizer registerView:1]; + [synchronizer registerView:2]; + [synchronizer beginResizeForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + [synchronizer beginResizeForView:2 + size:CGSize{15, 15} + notify:^{ + }]; + }]; + [scaffold joinMain]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + + // First frame. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + [synchronizer performCommitForView:2 + size:CGSize{15, 15} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Resize view 2 to (17, 17): blocks until the next frame. + [scaffold dispatchMainTask:^{ + [synchronizer beginResizeForView:2 + size:CGSize{17, 17} + notify:^{ + begunResizing->Signal(); + }]; + }]; + begunResizing->Wait(); + EXPECT_TRUE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Render view 1 with the size. Still blocking. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_TRUE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Render view 2 with the old size. Still blocking. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{15, 15} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_TRUE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Render view 1 with the size. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:1 + size:CGSize{5, 5} + notify:^{ + }]; + }]; + [scaffold joinRender]; + EXPECT_TRUE([synchronizer isWaitingWhenMutexIsAvailable]); + + // Render view 2 with the new size. Unblocks. + [scaffold dispatchRenderTask:^{ + [synchronizer performCommitForView:2 + size:CGSize{17, 17} + notify:^{ + }]; + }]; + [scaffold joinRender]; + [scaffold joinMain]; + EXPECT_FALSE([synchronizer isWaitingWhenMutexIsAvailable]); +} diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index f581079574bdc..0c468d4c58d08 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -44,7 +44,8 @@ constexpr FlutterViewId kFlutterDefaultViewId = 0ll; - (nullable instancetype)initWithMTLDevice:(nonnull id)device commandQueue:(nonnull id)commandQueue reshapeListener:(nonnull id)reshapeListener - NS_DESIGNATED_INITIALIZER; + threadSynchronizer:(nonnull FlutterThreadSynchronizer*)threadSynchronizer + viewId:(int64_t)viewId NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(nullable NSOpenGLPixelFormat*)format NS_UNAVAILABLE; @@ -58,12 +59,6 @@ constexpr FlutterViewId kFlutterDefaultViewId = 0ll; */ @property(readonly, nonatomic, nonnull) FlutterSurfaceManager* surfaceManager; -/** - * Must be called when shutting down. Unblocks raster thread and prevents any further - * synchronization. - */ -- (void)shutdown; - /** * By default, the `FlutterSurfaceManager` creates two layers to manage Flutter * content, the content layer and containing layer. To set the native background @@ -74,13 +69,3 @@ constexpr FlutterViewId kFlutterDefaultViewId = 0ll; - (void)setBackgroundColor:(nonnull NSColor*)color; @end - -@interface FlutterView (FlutterViewPrivate) - -/** - * Returns FlutterThreadSynchronizer for this view. - * Used for FlutterEngineTest. - */ -- (nonnull FlutterThreadSynchronizer*)threadSynchronizer; - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/shell/platform/darwin/macos/framework/Source/FlutterView.mm index c3f802d43ed9e..89bbdb9153828 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -10,6 +10,7 @@ #import @interface FlutterView () { + int64_t _viewId; __weak id _reshapeListener; FlutterThreadSynchronizer* _threadSynchronizer; FlutterSurfaceManager* _surfaceManager; @@ -21,14 +22,17 @@ @implementation FlutterView - (instancetype)initWithMTLDevice:(id)device commandQueue:(id)commandQueue - reshapeListener:(id)reshapeListener { + reshapeListener:(id)reshapeListener + threadSynchronizer:(FlutterThreadSynchronizer*)threadSynchronizer + viewId:(int64_t)viewId { self = [super initWithFrame:NSZeroRect]; if (self) { [self setWantsLayer:YES]; [self setBackgroundColor:[NSColor blackColor]]; [self setLayerContentsRedrawPolicy:NSViewLayerContentsRedrawDuringViewResize]; + _viewId = viewId; _reshapeListener = reshapeListener; - _threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; + _threadSynchronizer = threadSynchronizer; _surfaceManager = [[FlutterSurfaceManager alloc] initWithDevice:device commandQueue:commandQueue layer:self.layer @@ -38,23 +42,20 @@ - (instancetype)initWithMTLDevice:(id)device } - (void)onPresent:(CGSize)frameSize withBlock:(dispatch_block_t)block { - [_threadSynchronizer performCommit:frameSize notify:block]; + [_threadSynchronizer performCommitForView:_viewId size:frameSize notify:block]; } - (FlutterSurfaceManager*)surfaceManager { return _surfaceManager; } -- (FlutterThreadSynchronizer*)threadSynchronizer { - return _threadSynchronizer; -} - - (void)reshaped { CGSize scaledSize = [self convertSizeToBacking:self.bounds.size]; - [_threadSynchronizer beginResize:scaledSize - notify:^{ - [_reshapeListener viewDidReshape:self]; - }]; + [_threadSynchronizer beginResizeForView:_viewId + size:scaledSize + notify:^{ + [_reshapeListener viewDidReshape:self]; + }]; } - (void)setBackgroundColor:(NSColor*)color { @@ -112,9 +113,6 @@ - (BOOL)layer:(CALayer*)layer return YES; } -- (void)shutdown { - [_threadSynchronizer shutdown]; -} #pragma mark - NSAccessibility overrides - (BOOL)isAccessibilityElement { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index e23474159af4d..5129ef4ebe0f0 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -372,6 +372,10 @@ @implementation FlutterViewController { std::shared_ptr _bridge; FlutterViewId _id; + + // FlutterViewController does not actually uses the synchronizer, but only + // passes it to FlutterView. + FlutterThreadSynchronizer* _threadSynchronizer; } @synthesize viewId = _viewId; @@ -541,14 +545,20 @@ - (void)notifySemanticsEnabledChanged { return _bridge; } -- (void)attachToEngine:(nonnull FlutterEngine*)engine withId:(FlutterViewId)viewId { +- (void)setUpWithEngine:(FlutterEngine*)engine + viewId:(FlutterViewId)viewId + threadSynchronizer:(FlutterThreadSynchronizer*)threadSynchronizer { NSAssert(_engine == nil, @"Already attached to an engine %@.", _engine); _engine = engine; _viewId = viewId; + _threadSynchronizer = threadSynchronizer; + [_threadSynchronizer registerView:_viewId]; } - (void)detachFromEngine { NSAssert(_engine != nil, @"Not attached to any engine."); + [_threadSynchronizer deregisterView:_viewId]; + _threadSynchronizer = nil; _engine = nil; } @@ -858,7 +868,9 @@ - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id)device commandQueue:(id)commandQueue { return [[FlutterView alloc] initWithMTLDevice:device commandQueue:commandQueue - reshapeListener:self]; + reshapeListener:self + threadSynchronizer:_threadSynchronizer + viewId:_viewId]; } - (void)onKeyboardLayoutChanged { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index 62801f9bce739..4ffeffac4bb17 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -39,11 +39,14 @@ - (BOOL)isDispatchingKeyEvent:(nonnull NSEvent*)event; /** - * Set the `engine` and `id` of this controller. + * Set up the controller with `engine` and `id`, and other engine-level classes. * - * This method is called by FlutterEngine. + * This method is called by FlutterEngine. A view controller must be set up + * before being used, and must be set up only once until detachFromEngine:. */ -- (void)attachToEngine:(nonnull FlutterEngine*)engine withId:(FlutterViewId)viewId; +- (void)setUpWithEngine:(nonnull FlutterEngine*)engine + viewId:(FlutterViewId)viewId + threadSynchronizer:(nonnull FlutterThreadSynchronizer*)threadSynchronizer; /** * 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 15ca078042fd8..7599b66e8b0ec 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewTest.mm @@ -8,6 +8,8 @@ #import "flutter/testing/testing.h" +constexpr int64_t kDefaultViewId = 0ll; + @interface TestReshapeListener : NSObject @end @@ -23,8 +25,11 @@ - (void)viewDidReshape:(nonnull NSView*)view { id device = MTLCreateSystemDefaultDevice(); id queue = [device newCommandQueue]; TestReshapeListener* listener = [[TestReshapeListener alloc] init]; + FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; FlutterView* view = [[FlutterView alloc] initWithMTLDevice:device commandQueue:queue - reshapeListener:listener]; + reshapeListener:listener + threadSynchronizer:threadSynchronizer + viewId:kDefaultViewId]; EXPECT_EQ([view layer:view.layer shouldInheritContentsScale:3.0 fromWindow:view.window], YES); }