diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 8e517e61ac41c..e4c6e82e0e05f 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2717,8 +2717,6 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEngin FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterExternalTexture.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterExternalTexture.mm -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -2739,10 +2737,8 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatf FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.h -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.mm -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm @@ -2754,6 +2750,8 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextI FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextureRegistrar.h 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/FlutterView.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 935a3c3591978..6dc6b79a98146 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -71,8 +71,6 @@ source_set("flutter_framework_source") { "framework/Source/FlutterEngine_Internal.h", "framework/Source/FlutterExternalTexture.h", "framework/Source/FlutterExternalTexture.mm", - "framework/Source/FlutterIOSurfaceHolder.h", - "framework/Source/FlutterIOSurfaceHolder.mm", "framework/Source/FlutterKeyPrimaryResponder.h", "framework/Source/FlutterKeyboardManager.h", "framework/Source/FlutterKeyboardManager.mm", @@ -88,10 +86,8 @@ source_set("flutter_framework_source") { "framework/Source/FlutterPlatformViewController.mm", "framework/Source/FlutterRenderer.h", "framework/Source/FlutterRenderer.mm", - "framework/Source/FlutterResizableBackingStoreProvider.h", - "framework/Source/FlutterResizableBackingStoreProvider.mm", - "framework/Source/FlutterResizeSynchronizer.h", - "framework/Source/FlutterResizeSynchronizer.mm", + "framework/Source/FlutterSurface.h", + "framework/Source/FlutterSurface.mm", "framework/Source/FlutterSurfaceManager.h", "framework/Source/FlutterSurfaceManager.mm", "framework/Source/FlutterTextInputPlugin.h", @@ -100,6 +96,8 @@ 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/FlutterView.h", "framework/Source/FlutterView.mm", "framework/Source/FlutterViewController.mm", diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h index 66cf7b8939d37..99983e8ddb9bf 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h @@ -10,8 +10,8 @@ #include "flutter/fml/macros.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" -#include "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" -#include "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h" #include "flutter/shell/platform/embedder/embedder.h" namespace flutter { @@ -28,8 +28,7 @@ class FlutterCompositor { // It must not be null, and is typically FlutterViewEngineProvider. explicit FlutterCompositor( id view_provider, - FlutterPlatformViewController* platform_views_controller, - id mtl_device); + FlutterPlatformViewController* platform_views_controller); ~FlutterCompositor() = default; @@ -48,57 +47,12 @@ class FlutterCompositor { bool CreateBackingStore(const FlutterBackingStoreConfig* config, FlutterBackingStore* backing_store_out); - // Releases the memory for any resources that were allocated for the - // specified backing store. - bool CollectBackingStore(const FlutterBackingStore* backing_store); - // Presents the FlutterLayers by updating the FlutterView specified by // `view_id` using the layer content. Sets frame_started_ to false. bool Present(uint64_t view_id, const FlutterLayer** layers, size_t layers_count); - // Callback triggered at the end of the Present function. has_flutter_content - // is true when Flutter content was rendered, otherwise false. - using PresentCallback = std::function; - - // Registers a callback to be triggered at the end of the Present function. - // If a callback was previously registered, it will be replaced. - void SetPresentCallback(const PresentCallback& present_callback); - - // The status of the frame being composited. - // Started: A new frame has begun and we have cleared the old layer tree and - // are now creating backingstore(s) for the embedder to use. - // Presenting: the embedder has finished rendering into the provided - // backingstore(s) and we are creating the layer tree for the system - // compositor to present with. - // Ended: The frame has been presented and we are no longer processing it. - typedef enum { kStarted, kPresenting, kEnded } FrameStatus; - - protected: - // Returns the view associated with the view ID. - // - // Returns nil if the ID is invalid. - FlutterView* GetView(uint64_t view_id); - - // Gets and sets the FrameStatus for the current frame. - void SetFrameStatus(FrameStatus frame_status); - FrameStatus GetFrameStatus(); - - // Clears the previous CALayers and updates the frame status to frame started. - void StartFrame(); - - // Calls the present callback and ensures the frame status is updated - // to frame ended, returning whether the present was successful or not. - bool EndFrame(bool has_flutter_content); - - // Creates a CALayer object which is backed by the supplied IOSurface, and - // adds it to the root CALayer for the given view. - void InsertCALayerForIOSurface( - FlutterView* view, - const IOSurfaceRef& io_surface, - CATransform3D transform = CATransform3DIdentity); - private: // Presents the platform view layer represented by `layer`. `layer_index` is // used to position the layer in the z-axis. If the layer does not have a @@ -107,25 +61,12 @@ class FlutterCompositor { const FlutterLayer* layer, size_t layer_position); - // A list of the active CALayer objects for the frame that need to be removed. - std::list active_ca_layers_; - // Where the compositor can query FlutterViews. Must not be null. id const view_provider_; // The controller used to manage creation and deletion of platform views. const FlutterPlatformViewController* platform_view_controller_; - // The Metal device used to draw graphics. - const id mtl_device_; - - // Callback set by the embedder to be called when the layer tree has been - // correctly set up for this frame. - PresentCallback present_callback_; - - // Current frame status. - FrameStatus frame_status_ = kEnded; - FML_DISALLOW_COPY_AND_ASSIGN(FlutterCompositor); }; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm index ccdfaec180c2a..d71f5accafab4 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.mm @@ -5,16 +5,12 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h" #include "flutter/fml/logging.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h" namespace flutter { FlutterCompositor::FlutterCompositor(id view_provider, - FlutterPlatformViewController* platform_view_controller, - id mtl_device) - : view_provider_(view_provider), - platform_view_controller_(platform_view_controller), - mtl_device_(mtl_device) { + FlutterPlatformViewController* platform_view_controller) + : view_provider_(view_provider), platform_view_controller_(platform_view_controller) { FML_CHECK(view_provider != nullptr) << "view_provider cannot be nullptr"; } @@ -23,103 +19,67 @@ // TODO(dkwingsmt): This class only supports single-view for now. As more // classes are gradually converted to multi-view, it should get the view ID // from somewhere. - FlutterView* view = GetView(kFlutterDefaultViewId); + FlutterView* view = [view_provider_ getView:kFlutterDefaultViewId]; if (!view) { return false; } CGSize size = CGSizeMake(config->size.width, config->size.height); - - backing_store_out->metal.struct_size = sizeof(FlutterMetalBackingStore); - backing_store_out->metal.texture.struct_size = sizeof(FlutterMetalTexture); - - if (GetFrameStatus() != FrameStatus::kStarted) { - StartFrame(); - // If the backing store is for the first layer, return the MTLTexture for the - // FlutterView. - FlutterRenderBackingStore* backingStore = [view backingStoreForSize:size]; - backing_store_out->metal.texture.texture = - (__bridge FlutterMetalTextureHandle)backingStore.texture; - } else { - FlutterIOSurfaceHolder* io_surface_holder = [[FlutterIOSurfaceHolder alloc] init]; - [io_surface_holder recreateIOSurfaceWithSize:size]; - auto texture_descriptor = - [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm - width:size.width - height:size.height - mipmapped:NO]; - texture_descriptor.usage = - MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget | MTLTextureUsageShaderWrite; - - backing_store_out->metal.texture.texture = (__bridge_retained FlutterMetalTextureHandle) - [mtl_device_ newTextureWithDescriptor:texture_descriptor - iosurface:[io_surface_holder ioSurface] - plane:0]; - - backing_store_out->metal.texture.user_data = (__bridge_retained void*)io_surface_holder; - } - + FlutterSurface* surface = [view.surfaceManager surfaceForSize:size]; + memset(backing_store_out, 0, sizeof(FlutterBackingStore)); + backing_store_out->struct_size = sizeof(FlutterBackingStore); backing_store_out->type = kFlutterBackingStoreTypeMetal; - backing_store_out->metal.texture.destruction_callback = [](void* user_data) { - if (user_data != nullptr) { - CFRelease(user_data); - } - }; - - return true; -} - -bool FlutterCompositor::CollectBackingStore(const FlutterBackingStore* backing_store) { - // If we allocated this MTLTexture ourselves, user_data is not null, and we will need - // to release it manually. - if (backing_store->metal.texture.user_data != nullptr && - backing_store->metal.texture.texture != nullptr) { - CFRelease(backing_store->metal.texture.texture); - } + backing_store_out->metal.struct_size = sizeof(FlutterMetalBackingStore); + backing_store_out->metal.texture = surface.asFlutterMetalTexture; return true; } bool FlutterCompositor::Present(uint64_t view_id, const FlutterLayer** layers, size_t layers_count) { - FlutterView* view = GetView(view_id); + FlutterView* view = [view_provider_ getView:view_id]; if (!view) { return false; } - SetFrameStatus(FrameStatus::kPresenting); - - bool has_flutter_content = false; - for (size_t i = 0; i < layers_count; ++i) { - const auto* layer = layers[i]; - FlutterBackingStore* backing_store = const_cast(layer->backing_store); - - switch (layer->type) { - case kFlutterLayerContentTypeBackingStore: { - if (backing_store->metal.texture.user_data) { - FlutterIOSurfaceHolder* io_surface_holder = - (__bridge FlutterIOSurfaceHolder*)backing_store->metal.texture.user_data; - IOSurfaceRef io_surface = [io_surface_holder ioSurface]; - InsertCALayerForIOSurface(view, io_surface); - } - has_flutter_content = true; - break; - } - case kFlutterLayerContentTypePlatformView: { - PresentPlatformView(view, layer, i); - break; + NSMutableArray* surfaces = [NSMutableArray array]; + for (size_t i = 0; i < layers_count; i++) { + const FlutterLayer* layer = layers[i]; + if (layer->type == kFlutterLayerContentTypeBackingStore) { + FlutterSurface* surface = + [FlutterSurface fromFlutterMetalTexture:&layer->backing_store->metal.texture]; + + if (surface) { + FlutterSurfacePresentInfo* info = [[FlutterSurfacePresentInfo alloc] init]; + info.surface = surface; + info.offset = CGPointMake(layer->offset.x, layer->offset.y); + info.zIndex = i; + [surfaces addObject:info]; } - }; + } } - return EndFrame(has_flutter_content); + [view.surfaceManager present:surfaces + notify:^{ + for (size_t i = 0; i < layers_count; i++) { + const FlutterLayer* layer = layers[i]; + switch (layer->type) { + case kFlutterLayerContentTypeBackingStore: + break; + case kFlutterLayerContentTypePlatformView: + PresentPlatformView(view, layer, i); + break; + } + } + [platform_view_controller_ disposePlatformViews]; + }]; + + return true; } void FlutterCompositor::PresentPlatformView(FlutterView* default_base_view, const FlutterLayer* layer, size_t layer_position) { - // TODO (https://github.com/flutter/flutter/issues/96668) - // once the issue is fixed, this check will pass. FML_DCHECK([[NSThread currentThread] isMainThread]) << "Must be on the main thread to present platform views"; @@ -128,7 +88,7 @@ FML_DCHECK(platform_view) << "Platform view not found for id: " << platform_view_id; - CGFloat scale = [[NSScreen mainScreen] backingScaleFactor]; + CGFloat scale = platform_view.layer.contentsScale; platform_view.frame = CGRectMake(layer->offset.x / scale, layer->offset.y / scale, layer->size.width / scale, layer->size.height / scale); if (platform_view.superview == nil) { @@ -137,51 +97,4 @@ platform_view.layer.zPosition = layer_position; } -void FlutterCompositor::SetPresentCallback( - const FlutterCompositor::PresentCallback& present_callback) { - present_callback_ = present_callback; -} - -void FlutterCompositor::StartFrame() { - // First remove all CALayers from the superlayer. - for (auto layer : active_ca_layers_) { - [layer removeFromSuperlayer]; - } - - // Reset active layers. - active_ca_layers_.clear(); - SetFrameStatus(FrameStatus::kStarted); -} - -bool FlutterCompositor::EndFrame(bool has_flutter_content) { - bool status = present_callback_(has_flutter_content); - SetFrameStatus(FrameStatus::kEnded); - return status; -} - -FlutterView* FlutterCompositor::GetView(uint64_t view_id) { - return [view_provider_ getView:view_id]; -} - -void FlutterCompositor::SetFrameStatus(FlutterCompositor::FrameStatus frame_status) { - frame_status_ = frame_status; -} - -FlutterCompositor::FrameStatus FlutterCompositor::GetFrameStatus() { - return frame_status_; -} - -void FlutterCompositor::InsertCALayerForIOSurface(FlutterView* view, - const IOSurfaceRef& io_surface, - CATransform3D transform) { - // FlutterCompositor manages the lifecycle of CALayers. - CALayer* content_layer = [[CALayer alloc] init]; - content_layer.transform = transform; - content_layer.frame = view.layer.bounds; - [content_layer setContents:(__bridge id)io_surface]; - [view.layer addSublayer:content_layer]; - - active_ca_layers_.push_back(content_layer); -} - } // namespace flutter diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm index 576af2bbac152..bd6bf4d57b58b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositorTest.mm @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import #import #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h" @@ -41,13 +42,17 @@ - (nullable FlutterView*)getView:(uint64_t)viewId { namespace flutter::testing { namespace { -id MockViewProvider() { +typedef void (^PresentBlock)(NSArray*); + +id MockViewProvider(PresentBlock onPresent = nil) { FlutterView* viewMock = OCMClassMock([FlutterView class]); - FlutterRenderBackingStore* backingStoreMock = OCMClassMock([FlutterRenderBackingStore class]); + FlutterSurfaceManager* surfaceManagerMock = OCMClassMock([FlutterSurfaceManager class]); + FlutterSurface* surfaceMock = OCMClassMock([FlutterSurface class]); __block id textureMock = OCMProtocolMock(@protocol(MTLTexture)); - OCMStub([backingStoreMock texture]).andReturn(textureMock); - OCMStub([viewMock backingStoreForSize:CGSize{}]) + OCMStub([viewMock surfaceManager]).andReturn(surfaceManagerMock); + + OCMStub([surfaceManagerMock surfaceForSize:CGSize{}]) .ignoringNonObjectArgs() .andDo(^(NSInvocation* invocation) { CGSize size; @@ -55,31 +60,35 @@ - (nullable FlutterView*)getView:(uint64_t)viewId { OCMStub([textureMock width]).andReturn(size.width); OCMStub([textureMock height]).andReturn(size.height); }) - .andReturn(backingStoreMock); + .andReturn(surfaceMock); - return [[FlutterViewMockProvider alloc] initWithDefaultView:viewMock]; -} -} // namespace + FlutterMetalTexture texture = { + .struct_size = sizeof(FlutterMetalTexture), + .texture_id = 1, + .texture = (__bridge void*)textureMock, + .user_data = (__bridge void*)surfaceMock, + .destruction_callback = nullptr, + }; -TEST(FlutterCompositorTest, TestPresent) { - std::unique_ptr macos_compositor = - std::make_unique(MockViewProvider(), /*platform_view_controller*/ nullptr, - /*mtl_device*/ nullptr); + OCMStub([surfaceManagerMock present:[OCMArg any] notify:[OCMArg any]]) + .andDo(^(NSInvocation* invocation) { + NSArray* info; + [invocation getArgument:&info atIndex:2]; + if (onPresent != nil) { + onPresent(info); + } + }); - bool flag = false; - macos_compositor->SetPresentCallback([f = &flag](bool has_flutter_content) { - *f = true; - return true; - }); + OCMStub([surfaceMock asFlutterMetalTexture]).andReturn(texture); - ASSERT_TRUE(macos_compositor->Present(0, nil, 0)); - ASSERT_TRUE(flag); + return [[FlutterViewMockProvider alloc] initWithDefaultView:viewMock]; } +} // namespace TEST(FlutterCompositorTest, TestCreate) { std::unique_ptr macos_compositor = - std::make_unique(MockViewProvider(), /*platform_view_controller*/ nullptr, - /*mtl_device*/ nullptr); + std::make_unique(MockViewProvider(), + /*platform_view_controller*/ nullptr); FlutterBackingStore backing_store; FlutterBackingStoreConfig config; @@ -95,10 +104,16 @@ - (nullable FlutterView*)getView:(uint64_t)viewId { ASSERT_EQ(texture.height, 600ul); } -TEST(FlutterCompositorTest, TestCompositing) { +TEST(FlutterCompositorTest, TestPresent) { + __block NSArray* presentedSurfaces = nil; + + auto onPresent = ^(NSArray* info) { + presentedSurfaces = info; + }; + std::unique_ptr macos_compositor = - std::make_unique(MockViewProvider(), /*platform_view_controller*/ nullptr, - /*mtl_device*/ nullptr); + std::make_unique(MockViewProvider(onPresent), + /*platform_view_controller*/ nullptr); FlutterBackingStore backing_store; FlutterBackingStoreConfig config; @@ -107,11 +122,18 @@ - (nullable FlutterView*)getView:(uint64_t)viewId { config.size.height = 600; macos_compositor->CreateBackingStore(&config, &backing_store); - ASSERT_EQ(backing_store.type, kFlutterBackingStoreTypeMetal); - ASSERT_NE(backing_store.metal.texture.texture, nil); - id texture = (__bridge id)backing_store.metal.texture.texture; - ASSERT_EQ(texture.width, 800u); - ASSERT_EQ(texture.height, 600u); + FlutterLayer layers[] = {{ + .struct_size = sizeof(FlutterLayer), + .type = kFlutterLayerContentTypeBackingStore, + .backing_store = &backing_store, + .offset = {0, 0}, + .size = {800, 600}, + }}; + const FlutterLayer* layers_ptr = layers; + + macos_compositor->Present(kFlutterDefaultViewId, &layers_ptr, 1); + + ASSERT_EQ(presentedSurfaces.count, 1ul); } } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 19ed29aa1171c..870ca4802f3d9 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -432,22 +432,8 @@ - (FlutterCompositor*)createFlutterCompositor { return nil; } - __weak FlutterEngine* weakSelf = self; - - _macOSCompositor = std::make_unique( - _viewProvider, _platformViewController, _renderer.device); - _macOSCompositor->SetPresentCallback([weakSelf](bool has_flutter_content) { - // TODO(dkwingsmt): The compositor only supports single-view for now. As - // more classes are gradually converted to multi-view, it should get the - // view ID from somewhere. - uint64_t viewId = kFlutterDefaultViewId; - if (has_flutter_content) { - return [weakSelf.renderer present:viewId] == YES; - } else { - [weakSelf.renderer presentWithoutContent:viewId]; - return true; - } - }); + _macOSCompositor = + std::make_unique(_viewProvider, _platformViewController); _compositor = {}; _compositor.struct_size = sizeof(FlutterCompositor); @@ -463,10 +449,7 @@ - (FlutterCompositor*)createFlutterCompositor { _compositor.collect_backing_store_callback = [](const FlutterBackingStore* backing_store, // void* user_data // - ) { - return reinterpret_cast(user_data)->CollectBackingStore( - backing_store); - }; + ) { return true; }; _compositor.present_layers_callback = [](const FlutterLayer** layers, // size_t layers_count, // diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index 143b05b7ced8e..ad7535c83f157 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -30,6 +30,16 @@ @interface FlutterEngine (Test) @property(nonatomic, readonly, nullable) flutter::FlutterCompositor* macOSCompositor; @end +@interface TestPlatformViewFactory : NSObject +@end + +@implementation TestPlatformViewFactory +- (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable id)args { + return viewId == 42 ? [[NSView alloc] init] : nil; +} + +@end + namespace flutter::testing { TEST_F(FlutterEngineTest, CanLaunch) { @@ -447,8 +457,7 @@ @interface FlutterEngine (Test) ASSERT_TRUE(latch_called); } -// TODO(iskakaushik): Enable after https://github.com/flutter/flutter/issues/96668 is fixed. -TEST(FlutterEngine, DISABLED_Compositor) { +TEST(FlutterEngine, Compositor) { NSString* fixtures = @(flutter::testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] initWithAssetsPath:fixtures @@ -462,26 +471,29 @@ @interface FlutterEngine (Test) EXPECT_TRUE([engine runWithEntrypoint:@"canCompositePlatformViews"]); - // Latch to ensure the entire layer tree has been generated and presented. - fml::AutoResetWaitableEvent latch; - auto compositor = engine.macOSCompositor; - compositor->SetPresentCallback([&](bool has_flutter_content) { - latch.Signal(); - return true; - }); - latch.Wait(); + [engine.platformViewController registerViewFactory:[[TestPlatformViewFactory alloc] init] + withId:@"factory_id"]; + [engine.platformViewController + handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create" + arguments:@{ + @"id" : @(42), + @"viewType" : @"factory_id", + }] + result:^(id result){ + }]; + + [viewController.flutterView.threadSynchronizer blockUntilFrameAvailable]; CALayer* rootLayer = viewController.flutterView.layer; - // There are three layers total - the root layer and two sublayers. - // This test will need to be updated when PlatformViews are supported, as - // there are two PlatformView layers in this test. + // There are two layers with Flutter contents and one view EXPECT_EQ(rootLayer.sublayers.count, 2u); + EXPECT_EQ(viewController.flutterView.subviews.count, 1u); // TODO(gw280): add support for screenshot tests in this test harness [engine shutDownEngine]; -} +} // namespace flutter::testing TEST(FlutterEngine, DartEntrypointArguments) { NSString* fixtures = @(flutter::testing::GetFixturesPath()); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h b/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h deleted file mode 100644 index e3a1f8a7113dd..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h +++ /dev/null @@ -1,24 +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 - -/** - * FlutterIOSurfaceHolder maintains an IOSurface - * and provides an interface to bind the IOSurface to a texture. - */ -@interface FlutterIOSurfaceHolder : NSObject - -/** - * Releases the current IOSurface if one exists - * and creates a new IOSurface with the specified size. - */ -- (void)recreateIOSurfaceWithSize:(CGSize)size; - -/** - * Returns a reference to the underlying IOSurface. - */ -- (const IOSurfaceRef&)ioSurface; - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.mm b/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.mm deleted file mode 100644 index d8553507442f9..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.mm +++ /dev/null @@ -1,47 +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/FlutterIOSurfaceHolder.h" - -@interface FlutterIOSurfaceHolder () { - IOSurfaceRef _ioSurface; -} -@end - -@implementation FlutterIOSurfaceHolder - -- (void)recreateIOSurfaceWithSize:(CGSize)size { - if (_ioSurface) { - CFRelease(_ioSurface); - } - - unsigned pixelFormat = 'BGRA'; - unsigned bytesPerElement = 4; - - size_t bytesPerRow = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size.width * bytesPerElement); - size_t totalBytes = IOSurfaceAlignProperty(kIOSurfaceAllocSize, size.height * bytesPerRow); - NSDictionary* options = @{ - (id)kIOSurfaceWidth : @(size.width), - (id)kIOSurfaceHeight : @(size.height), - (id)kIOSurfacePixelFormat : @(pixelFormat), - (id)kIOSurfaceBytesPerElement : @(bytesPerElement), - (id)kIOSurfaceBytesPerRow : @(bytesPerRow), - (id)kIOSurfaceAllocSize : @(totalBytes), - }; - - _ioSurface = IOSurfaceCreate((CFDictionaryRef)options); - IOSurfaceSetValue(_ioSurface, CFSTR("IOSurfaceColorSpace"), kCGColorSpaceSRGB); -} - -- (const IOSurfaceRef&)ioSurface { - return _ioSurface; -} - -- (void)dealloc { - if (_ioSurface) { - CFRelease(_ioSurface); - } -} - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.mm index b15b1ca5ebd42..ac01ef57bc1cf 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.mm @@ -53,6 +53,9 @@ - (void)onCreateWithViewID:(int64_t)viewId } NSView* platform_view = [factory createWithViewIdentifier:viewId arguments:nil]; + // Flutter compositing requires CALayer-backed platform views. + // Force the platform view to be backed by a CALayer. + [platform_view setWantsLayer:YES]; _platformViews[viewId] = platform_view; result(nil); } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h index d9cbe1da5abb0..483a858032bc8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h @@ -38,12 +38,7 @@ /** * Called by the engine when the given view's buffers should be swapped. */ -- (BOOL)present:(uint64_t)viewId; - -/** - * Tells the renderer that there is no Flutter content available for this frame. - */ -- (void)presentWithoutContent:(uint64_t)viewId; +- (BOOL)present:(uint64_t)viewId texture:(nonnull const FlutterMetalTexture*)texture; /** * Creates a Metal texture for the given view with the given size. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm index 996e613dba926..007a3cce157a5 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterRenderer.mm @@ -28,7 +28,7 @@ static bool OnPresentDrawableOfDefaultView(FlutterEngine* engine, // operates on the default view. To support multi-view, we need a new callback // that also receives a view ID. uint64_t viewId = kFlutterDefaultViewId; - return [engine.renderer present:viewId]; + return [engine.renderer present:viewId texture:texture]; } static bool OnAcquireExternalTexture(FlutterEngine* engine, @@ -95,30 +95,22 @@ - (FlutterMetalTexture)createTextureForView:(uint64_t)viewId size:(CGSize)size { // FlutterMetalTexture has texture `null`, therefore is discarded. return FlutterMetalTexture{}; } - FlutterRenderBackingStore* backingStore = [view backingStoreForSize:size]; - id texture = backingStore.texture; - FlutterMetalTexture embedderTexture; - embedderTexture.struct_size = sizeof(FlutterMetalTexture); - embedderTexture.texture = (__bridge void*)texture; - embedderTexture.texture_id = reinterpret_cast(texture); - return embedderTexture; + return [view.surfaceManager surfaceForSize:size].asFlutterMetalTexture; } -- (BOOL)present:(uint64_t)viewId { +- (BOOL)present:(uint64_t)viewId texture:(const FlutterMetalTexture*)texture { FlutterView* view = [_viewProvider getView:viewId]; if (view == nil) { return NO; } - [view present]; - return YES; -} - -- (void)presentWithoutContent:(uint64_t)viewId { - FlutterView* view = [_viewProvider getView:viewId]; - if (view == nil) { - return; + FlutterSurface* surface = [FlutterSurface fromFlutterMetalTexture:texture]; + if (surface == nil) { + return NO; } - [view presentWithoutContent]; + FlutterSurfacePresentInfo* info = [[FlutterSurfacePresentInfo alloc] init]; + info.surface = surface; + [view.surfaceManager present:@[ info ] notify:nil]; + return YES; } #pragma mark - FlutterTextureRegistrar methods. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm index 6cd57e33d48f9..01146e1464c02 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterRendererTest.mm @@ -37,17 +37,39 @@ void SetEngineDefaultView(FlutterEngine* engine, id flutterView) { TEST(FlutterRenderer, PresentDelegatesToFlutterView) { FlutterEngine* engine = CreateTestEngine(); FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine]; - id mockFlutterView = OCMClassMock([FlutterView class]); - SetEngineDefaultView(engine, mockFlutterView); - [(FlutterView*)[mockFlutterView expect] present]; - [renderer present:kFlutterDefaultViewId]; + + id viewMock = OCMClassMock([FlutterView class]); + SetEngineDefaultView(engine, viewMock); + + id surfaceManagerMock = OCMClassMock([FlutterSurfaceManager class]); + OCMStub([viewMock surfaceManager]).andReturn(surfaceManagerMock); + + id surfaceMock = OCMClassMock([FlutterSurface class]); + + FlutterMetalTexture texture = { + .user_data = (__bridge void*)surfaceMock, + }; + + [[surfaceManagerMock expect] present:[OCMArg checkWithBlock:^(id obj) { + NSArray* array = (NSArray*)obj; + return array.count == 1 ? YES : NO; + }] + notify:nil]; + + [renderer present:kFlutterDefaultViewId texture:&texture]; + [surfaceManagerMock verify]; } TEST(FlutterRenderer, TextureReturnedByFlutterView) { FlutterEngine* engine = CreateTestEngine(); FlutterRenderer* renderer = [[FlutterRenderer alloc] initWithFlutterEngine:engine]; - id mockFlutterView = OCMClassMock([FlutterView class]); - SetEngineDefaultView(engine, mockFlutterView); + + id viewMock = OCMClassMock([FlutterView class]); + SetEngineDefaultView(engine, viewMock); + + id surfaceManagerMock = OCMClassMock([FlutterSurfaceManager class]); + OCMStub([viewMock surfaceManager]).andReturn(surfaceManagerMock); + FlutterFrameInfo frameInfo; frameInfo.struct_size = sizeof(FlutterFrameInfo); FlutterUIntSize dimensions; @@ -55,8 +77,10 @@ void SetEngineDefaultView(FlutterEngine* engine, id flutterView) { dimensions.height = 200; frameInfo.size = dimensions; CGSize size = CGSizeMake(dimensions.width, dimensions.height); - [[mockFlutterView expect] backingStoreForSize:size]; + + [[surfaceManagerMock expect] surfaceForSize:size]; [renderer createTextureForView:kFlutterDefaultViewId size:size]; + [surfaceManagerMock verify]; } } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.h b/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.h deleted file mode 100644 index 00a93ac213078..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.h +++ /dev/null @@ -1,33 +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 -#import -#import - -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h" - -/** - * Provides resizable buffers backed by a MTLTexture. - */ -@interface FlutterResizableBackingStoreProvider : NSObject - -/** - * Creates a resizable backing store provider for the given CAMetalLayer. - */ -- (nonnull instancetype)initWithDevice:(nonnull id)device - commandQueue:(nonnull id)commandQueue - layer:(nonnull CALayer*)layer; -/** - * Notify of the required backing store size updates. Called during window resize. - */ -- (void)onBackingStoreResized:(CGSize)size; - -/** - * Returns the FlutterBackingStore corresponding to the latest size. - */ -- (nonnull FlutterRenderBackingStore*)backingStore; - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.mm b/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.mm deleted file mode 100644 index 436d41af68f15..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.mm +++ /dev/null @@ -1,54 +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/FlutterResizableBackingStoreProvider.h" - -#import - -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" - -@implementation FlutterResizableBackingStoreProvider { - id _device; - id _commandQueue; - FlutterSurfaceManager* _surfaceManager; -} - -- (instancetype)initWithDevice:(id)device - commandQueue:(id)commandQueue - layer:(CALayer*)layer { - self = [super init]; - if (self) { - _device = device; - _commandQueue = commandQueue; - _surfaceManager = [[FlutterSurfaceManager alloc] initWithDevice:device - commandQueue:commandQueue - layer:layer]; - } - return self; -} - -- (void)onBackingStoreResized:(CGSize)size { - [_surfaceManager ensureSurfaceSize:size]; -} - -- (FlutterRenderBackingStore*)backingStore { - return [_surfaceManager renderBuffer]; -} - -- (void)resizeSynchronizerFlush:(nonnull FlutterResizeSynchronizer*)synchronizer { - id commandBuffer = [_commandQueue commandBuffer]; - [commandBuffer commit]; - [commandBuffer waitUntilScheduled]; -} - -- (void)resizeSynchronizerCommit:(nonnull FlutterResizeSynchronizer*)synchronizer { - [CATransaction begin]; - [CATransaction setDisableActions:YES]; - - [_surfaceManager swapBuffers]; - - [CATransaction commit]; -} - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h deleted file mode 100644 index 556856ed3684d..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h +++ /dev/null @@ -1,90 +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 - -@class FlutterResizeSynchronizer; - -/** - * Implemented by FlutterView. - */ -@protocol FlutterResizeSynchronizerDelegate - -/** - * Invoked on raster thread; Delegate should flush the graphics context. - */ -- (void)resizeSynchronizerFlush:(nonnull FlutterResizeSynchronizer*)synchronizer; - -/** - * Invoked on platform thread; Delegate should flip the surfaces. - */ -- (void)resizeSynchronizerCommit:(nonnull FlutterResizeSynchronizer*)synchronizer; - -@end - -/** - * Encapsulates the logic for blocking platform thread during window resize as - * well as synchronizing the raster and platform thread during commit (presenting frame). - * - * Flow during window resize - * - * 1. Platform thread calls [synchronizer beginResize:notify:] - * This will hold the platform thread until the raster thread is ready to display contents. - * 2. Raster thread calls [synchronizer shouldEnsureSurfaceForSize:] with target size - * This will return false for any size other than target size - * 3. Raster thread calls [synchronizer requestCommit] - * Any commit calls before shouldEnsureSurfaceForSize: is called with the right - * size are simply ignored; There's no point rasterizing and displaying frames - * with wrong size. - * Both delegate methods (flush/commit) will be invoked before beginResize returns - * - * Flow during regular operation (no resizing) - * - * 1. Raster thread calls [synchronizer requestCommit] - * This will invoke [delegate flush:] on raster thread and - * [delegate commit:] on platform thread. The requestCommit call will be blocked - * until this is done. This is necessary to ensure that rasterizer won't start - * rasterizing next frame before the FlutterSurfaceManager flipped the surface, - * which must be performed on platform thread. - */ -@interface FlutterResizeSynchronizer : NSObject - -- (nullable instancetype)initWithDelegate:(nonnull id)delegate; - -/** - * Blocks the platform thread until - * - shouldEnsureSurfaceForSize is called with proper size and - * - requestCommit is called - * All requestCommit calls before `shouldEnsureSurfaceForSize` is called with - * expected size are ignored; - * The notify block is invoked immediately after synchronizer mutex is acquired. - */ -- (void)beginResize:(CGSize)size notify:(nonnull dispatch_block_t)notify; - -/** - * Returns whether the view should ensure surfaces with given size; - * This will be false during resizing for any size other than size specified - * during beginResize. - */ -- (BOOL)shouldEnsureSurfaceForSize:(CGSize)size; - -/** - * Called from rasterizer thread, will block until delegate resizeSynchronizerCommit: - * method is called (on platform thread). - */ -- (void)requestCommit; - -/** - * Called from view to notify the synchronizer that there are no Flutter frames - * coming. Synchronizer must unblock main thread and not block until another - * frame is available. - */ -- (void)noFlutterContent; - -/** - * Called when shutting down. Unblocks everything and prevents any further synchronization. - */ -- (void)shutdown; - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm deleted file mode 100644 index 7681aa3385779..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm +++ /dev/null @@ -1,153 +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/FlutterResizeSynchronizer.h" - -#include - -@interface FlutterResizeSynchronizer () { - // Counter to detect stale callbacks. - uint32_t _cookie; - - std::mutex _mutex; - - // Used to block [beginResize:]. - std::condition_variable _condBlockBeginResize; - // Used to block [requestCommit]. - std::condition_variable _condBlockRequestCommit; - - // Whether a frame was received; the synchronizer doesn't block platform thread during resize - // until it knows that framework is running and producing frames - BOOL _hasFlutterContent; - - // If NO, requestCommit calls are ignored until shouldEnsureSurfaceForSize is called with - // proper size. - BOOL _acceptingCommit; - - // Waiting for resize to finish. - BOOL _waiting; - - // RequestCommit was called and [delegate commit:] must be performed on platform thread. - BOOL _pendingCommit; - - // Target size for resizing. - CGSize _newSize; - - // if YES prevents all synchronization - BOOL _shuttingDown; - - __weak id _delegate; -} -@end - -@implementation FlutterResizeSynchronizer - -- (instancetype)initWithDelegate:(id)delegate { - if (self = [super init]) { - _acceptingCommit = YES; - _delegate = delegate; - } - return self; -} - -- (void)beginResize:(CGSize)size notify:(dispatch_block_t)notify { - std::unique_lock lock(_mutex); - if (!_delegate) { - return; - } - - if (!_hasFlutterContent || _shuttingDown) { - // No blocking until framework produces at least one frame - notify(); - return; - } - - ++_cookie; - - // from now on, ignore all incoming commits until the block below gets - // scheduled on raster thread - _acceptingCommit = NO; - - // let pending commits finish to unblock the raster thread - _pendingCommit = NO; - _condBlockBeginResize.notify_all(); - - // let the engine send resize notification - notify(); - - _newSize = size; - - _waiting = YES; - - _condBlockRequestCommit.wait(lock, [&] { return _pendingCommit || _shuttingDown; }); - - [_delegate resizeSynchronizerFlush:self]; - [_delegate resizeSynchronizerCommit:self]; - _pendingCommit = NO; - _condBlockBeginResize.notify_all(); - - _waiting = NO; -} - -- (BOOL)shouldEnsureSurfaceForSize:(CGSize)size { - std::unique_lock lock(_mutex); - - if (!_hasFlutterContent) { - return YES; - } - - if (!_acceptingCommit) { - if (CGSizeEqualToSize(_newSize, size)) { - _acceptingCommit = YES; - } - } - return _acceptingCommit; -} - -- (void)requestCommit { - std::unique_lock lock(_mutex); - - if (!_acceptingCommit || _shuttingDown) { - return; - } - - _hasFlutterContent = YES; - - _pendingCommit = YES; - if (_waiting) { // BeginResize is in progress, interrupt it and schedule commit call - _condBlockRequestCommit.notify_all(); - _condBlockBeginResize.wait(lock, [&]() { return !_pendingCommit || _shuttingDown; }); - } else { - // No resize, schedule commit on platform thread and wait until either done - // or interrupted by incoming BeginResize - [_delegate resizeSynchronizerFlush:self]; - dispatch_async(dispatch_get_main_queue(), [self, cookie = _cookie] { - std::unique_lock lock(_mutex); - if (cookie == _cookie) { - if (_delegate) { - [_delegate resizeSynchronizerCommit:self]; - } - _pendingCommit = NO; - _condBlockBeginResize.notify_all(); - } - }); - _condBlockBeginResize.wait(lock, [&]() { return !_pendingCommit || _shuttingDown; }); - } -} - -- (void)noFlutterContent { - std::unique_lock lock(_mutex); - _hasFlutterContent = NO; - _acceptingCommit = YES; - _condBlockBeginResize.notify_all(); -} - -- (void)shutdown { - std::unique_lock lock(_mutex); - _shuttingDown = YES; - _condBlockBeginResize.notify_all(); - _condBlockRequestCommit.notify_all(); -} - -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurface.h b/shell/platform/darwin/macos/framework/Source/FlutterSurface.h new file mode 100644 index 0000000000000..697bd29dee81c --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurface.h @@ -0,0 +1,33 @@ +// 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 + +#import "flutter/shell/platform/embedder/embedder.h" + +/** + * Opaque surface type. + * Can be represented as FlutterMetalTexture to cross the embedder API boundary. + */ +@interface FlutterSurface : NSObject + +- (FlutterMetalTexture)asFlutterMetalTexture; + ++ (nullable FlutterSurface*)fromFlutterMetalTexture:(nonnull const FlutterMetalTexture*)texture; + +@end + +/** + * Internal FlutterSurface interface used by FlutterSurfaceManager. + * Wraps an IOSurface framebuffer and metadata related to the surface. + */ +@interface FlutterSurface (Private) + +- (nonnull instancetype)initWithSize:(CGSize)size device:(nonnull id)device; + +@property(readonly, nonatomic, nonnull) IOSurfaceRef ioSurface; +@property(readonly, nonatomic) CGSize size; +@property(readonly, nonatomic) int64_t textureId; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm b/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm new file mode 100644 index 0000000000000..4b65f2553c315 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurface.mm @@ -0,0 +1,98 @@ +// 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/FlutterSurface.h" + +#import + +@interface FlutterSurface () { + CGSize _size; + IOSurfaceRef _ioSurface; + id _texture; +} +@end + +@implementation FlutterSurface + +- (IOSurfaceRef)ioSurface { + return _ioSurface; +} + +- (CGSize)size { + return _size; +} + +- (int64_t)textureId { + return reinterpret_cast(_texture); +} + +- (instancetype)initWithSize:(CGSize)size device:(id)device { + if (self = [super init]) { + self->_size = size; + self->_ioSurface = [FlutterSurface createIOSurfaceWithSize:size]; + self->_texture = [FlutterSurface createTextureForIOSurface:_ioSurface size:size device:device]; + } + return self; +} + +static void ReleaseSurface(void* surface) { + if (surface != nullptr) { + CFBridgingRelease(surface); + } +} + +- (FlutterMetalTexture)asFlutterMetalTexture { + FlutterMetalTexture res; + memset(&res, 0, sizeof(FlutterMetalTexture)); + res.struct_size = sizeof(FlutterMetalTexture); + res.texture = (__bridge void*)_texture; + res.texture_id = self.textureId; + res.user_data = (void*)CFBridgingRetain(self); + res.destruction_callback = ReleaseSurface; + return res; +} + ++ (FlutterSurface*)fromFlutterMetalTexture:(const FlutterMetalTexture*)texture { + return (__bridge FlutterSurface*)texture->user_data; +} + +- (void)dealloc { + CFRelease(_ioSurface); +} + ++ (IOSurfaceRef)createIOSurfaceWithSize:(CGSize)size { + unsigned pixelFormat = 'BGRA'; + unsigned bytesPerElement = 4; + + size_t bytesPerRow = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size.width * bytesPerElement); + size_t totalBytes = IOSurfaceAlignProperty(kIOSurfaceAllocSize, size.height * bytesPerRow); + NSDictionary* options = @{ + (id)kIOSurfaceWidth : @(size.width), + (id)kIOSurfaceHeight : @(size.height), + (id)kIOSurfacePixelFormat : @(pixelFormat), + (id)kIOSurfaceBytesPerElement : @(bytesPerElement), + (id)kIOSurfaceBytesPerRow : @(bytesPerRow), + (id)kIOSurfaceAllocSize : @(totalBytes), + }; + + IOSurfaceRef res = IOSurfaceCreate((CFDictionaryRef)options); + IOSurfaceSetValue(res, CFSTR("IOSurfaceColorSpace"), kCGColorSpaceSRGB); + return res; +} + ++ (id)createTextureForIOSurface:(IOSurfaceRef)surface + size:(CGSize)size + device:(id)device { + MTLTextureDescriptor* textureDescriptor = + [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:size.width + height:size.height + mipmapped:NO]; + textureDescriptor.usage = + MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget | MTLTextureUsageShaderWrite; + // plane = 0 for BGRA. + return [device newTextureWithDescriptor:textureDescriptor iosurface:surface plane:0]; +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h index 2f6a0974e4164..da1e2bcc9b988 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h @@ -3,42 +3,100 @@ // found in the LICENSE file. #import -#import +#import -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h" /** - * Manages render surfaces and corresponding backing stores used by the engine. + * Surface with additional properties needed for presenting. + */ +@interface FlutterSurfacePresentInfo : NSObject + +@property(readwrite, strong, nonatomic, nonnull) FlutterSurface* surface; +@property(readwrite, nonatomic) CGPoint offset; +@property(readwrite, nonatomic) size_t zIndex; + +@end + +@protocol FlutterSurfaceManagerDelegate + +/* + * Schedules the block on the platform thread and blocks until the block is executed. + * 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; + +@end + +/** + * FlutterSurfaceManager is responsible for providing and presenting Core Animation render + * surfaces and managing sublayers. * - * The backing store when rendering with on Metal is a Metal texture. There are two IOSurfaces - * created during initialization, FlutterSurfaceManager manages the lifecycle of these. + * Owned by `FlutterView`. */ @interface FlutterSurfaceManager : NSObject /** * Initializes and returns a surface manager that renders to a child layer (referred to as the - * content layer) of the containing layer and applies the transform to the contents of the content - * layer. + * content layer) of the containing layer. */ - (nullable instancetype)initWithDevice:(nonnull id)device commandQueue:(nonnull id)commandQueue - layer:(nonnull CALayer*)containingLayer; + layer:(nonnull CALayer*)containingLayer + delegate:(nonnull id)delegate; + +/** + * Returns a back buffer surface of the given size to which Flutter can render content. + * A cached surface will be returned if available; otherwise a new one will be created. + * + * Must be called on raster thread. + */ +- (nonnull FlutterSurface*)surfaceForSize:(CGSize)size; /** - * Updates the backing store size of the managed IOSurfaces the specified size. If the surfaces are - * already of this size, this is a no-op. + * Sets the provided surfaces as contents of FlutterView. Will create, update and + * remove sublayers as needed. + * + * Must be called on raster thread. This will schedule a commit on the platform thread and block the + * raster thread until the commit is done. The `notify` block will be invoked on the platform thread + * 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*)surfaces + notify:(nullable dispatch_block_t)notify; + +@end + +/** + * Cache of back buffers to prevent unnecessary IOSurface allocations. */ -- (void)ensureSurfaceSize:(CGSize)size; +@interface FlutterBackBufferCache : NSObject /** - * Swaps the front and the back buffer. + * Removes surface with given size from cache (if available) and returns it. */ -- (void)swapBuffers; +- (nullable FlutterSurface*)removeSurfaceForSize:(CGSize)size; /** - * Returns the backing store for the back buffer. + * Removes all cached surfaces replacing them with new ones. */ -- (nonnull FlutterRenderBackingStore*)renderBuffer; +- (void)replaceSurfaces:(nonnull NSArray*)surfaces; + +/** + * Returns number of surfaces currently in cache. Used for tests. + */ +- (NSUInteger)count; + +@end + +/** + * Interface to internal properties used for testing. + */ +@interface FlutterSurfaceManager (Private) + +@property(readonly, nonatomic, nonnull) FlutterBackBufferCache* backBufferCache; +@property(readonly, nonatomic, nonnull) NSArray* frontSurfaces; +@property(readonly, nonatomic, nonnull) NSArray* layers; @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm index d3d612fd294c8..50d7be11117da 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm @@ -3,159 +3,201 @@ // found in the LICENSE file. #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h" #import - #include -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h" +@implementation FlutterSurfacePresentInfo +@end -enum { - kFlutterSurfaceManagerFrontBuffer = 0, - kFlutterSurfaceManagerBackBuffer = 1, - kFlutterSurfaceManagerBufferCount, -}; +@interface FlutterSurfaceManager () { + id _device; + id _commandQueue; + CALayer* _containingLayer; + __weak id _delegate; -// BackBuffer will be released after kIdleDelay if there is no activity. -static const double kIdleDelay = 1.0; + // Available (cached) back buffer surfaces. These will be cleared during + // present and replaced by current frong surfaces. + FlutterBackBufferCache* _backBufferCache; -@interface FlutterSurfaceManager () + // Surfaces currently used to back visible layers. + NSMutableArray* _frontSurfaces; -/** - * Cancels any previously-scheduled onIdle requests. - */ -- (void)cancelIdle; + // Currently visible layers. + NSMutableArray* _layers; +} /** - * Creates a backing textures for the specified surface with the specified size. + * Updates underlying CALayers with the contents of the surfaces to present. */ -- (id)createTextureForSurface:(FlutterIOSurfaceHolder*)surface size:(CGSize)size; +- (void)commit:(NSArray*)surfaces; @end -@implementation FlutterSurfaceManager { - CALayer* _containingLayer; // provided (parent layer) - CALayer* _contentLayer; - CATransform3D _contentTransform; +@implementation FlutterSurfaceManager - CGSize _surfaceSize; - FlutterIOSurfaceHolder* _ioSurfaces[kFlutterSurfaceManagerBufferCount]; - BOOL _frameInProgress; +- (instancetype)initWithDevice:(id)device + commandQueue:(id)commandQueue + layer:(CALayer*)containingLayer + delegate:(__weak id)delegate { + if (self = [super init]) { + _device = device; + _commandQueue = commandQueue; + _containingLayer = containingLayer; + _delegate = delegate; - id _device; - id _commandQueue; - id _textures[kFlutterSurfaceManagerBufferCount]; + _backBufferCache = [[FlutterBackBufferCache alloc] init]; + _frontSurfaces = [NSMutableArray array]; + _layers = [NSMutableArray array]; + } + return self; } -- (nullable instancetype)initWithDevice:(nonnull id)device - commandQueue:(nonnull id)commandQueue - layer:(nonnull CALayer*)containingLayer { - self = [super init]; - if (self) { - _containingLayer = containingLayer; - _contentTransform = CATransform3DIdentity; - _contentLayer = [[CALayer alloc] init]; - [_containingLayer addSublayer:_contentLayer]; +- (FlutterBackBufferCache*)backBufferCache { + return _backBufferCache; +} - _ioSurfaces[0] = [[FlutterIOSurfaceHolder alloc] init]; - _ioSurfaces[1] = [[FlutterIOSurfaceHolder alloc] init]; +- (NSArray*)frontSurfaces { + return _frontSurfaces; +} - _device = device; - _commandQueue = commandQueue; +- (NSArray*)layers { + return _layers; +} + +- (FlutterSurface*)surfaceForSize:(CGSize)size { + FlutterSurface* surface = [_backBufferCache removeSurfaceForSize:size]; + if (surface == nil) { + surface = [[FlutterSurface alloc] initWithSize:size device:_device]; } - return self; + return surface; } -- (void)ensureSurfaceSize:(CGSize)size { - if (CGSizeEqualToSize(size, _surfaceSize)) { - return; +- (void)commit:(NSArray*)surfaces { + assert([NSThread isMainThread]); + + // Release all unused back buffer surfaces and replace them with front surfaces. + [_backBufferCache replaceSurfaces:_frontSurfaces]; + + // Front surfaces will be replaced by currently presented surfaces. + [_frontSurfaces removeAllObjects]; + for (FlutterSurfacePresentInfo* info in surfaces) { + [_frontSurfaces addObject:info.surface]; } - _surfaceSize = size; - for (int i = 0; i < kFlutterSurfaceManagerBufferCount; ++i) { - if (_ioSurfaces[i] != nil) { - [_ioSurfaces[i] recreateIOSurfaceWithSize:size]; - _textures[i] = [self createTextureForSurface:_ioSurfaces[i] size:size]; - } + + // Add or remove layers to match the count of surfaces to present. + while (_layers.count > _frontSurfaces.count) { + [_layers.lastObject removeFromSuperlayer]; + [_layers removeLastObject]; + } + while (_layers.count < _frontSurfaces.count) { + CALayer* layer = [CALayer layer]; + [_containingLayer addSublayer:layer]; + [_layers addObject:layer]; + } + + // Update contents of surfaces. + for (size_t i = 0; i < surfaces.count; ++i) { + FlutterSurfacePresentInfo* info = surfaces[i]; + CALayer* layer = _layers[i]; + CGFloat scale = _containingLayer.contentsScale; + layer.frame = CGRectMake(info.offset.x / scale, info.offset.y / scale, + info.surface.size.width / scale, info.surface.size.height / scale); + layer.contents = (__bridge id)info.surface.ioSurface; + layer.zPosition = info.zIndex; } } -- (void)swapBuffers { -#ifndef NDEBUG - // swapBuffers should not be called unless a frame was drawn - @synchronized(self) { - assert(_frameInProgress); +static CGSize GetRequiredFrameSize(NSArray* surfaces) { + CGSize size = CGSizeZero; + for (FlutterSurfacePresentInfo* info in surfaces) { + size = CGSizeMake(std::max(size.width, info.offset.x + info.surface.size.width), + std::max(size.height, info.offset.y + info.surface.size.height)); } -#endif + return size; +} - _contentLayer.frame = _containingLayer.bounds; - _contentLayer.transform = _contentTransform; - IOSurfaceRef contentIOSurface = [_ioSurfaces[kFlutterSurfaceManagerBackBuffer] ioSurface]; - [_contentLayer setContents:(__bridge id)contentIOSurface]; +- (void)present:(NSArray*)surfaces notify:(dispatch_block_t)notify { + id commandBuffer = [_commandQueue commandBuffer]; + [commandBuffer commit]; + [commandBuffer waitUntilScheduled]; + + // Get the actual dimensions of the frame (relevant for thread synchronizer). + CGSize size = GetRequiredFrameSize(surfaces); + + [_delegate onPresent:size + withBlock:^{ + [self commit:surfaces]; + if (notify != nil) { + notify(); + } + }]; +} - std::swap(_ioSurfaces[kFlutterSurfaceManagerBackBuffer], - _ioSurfaces[kFlutterSurfaceManagerFrontBuffer]); - std::swap(_textures[kFlutterSurfaceManagerBackBuffer], - _textures[kFlutterSurfaceManagerFrontBuffer]); +@end - // performSelector:withObject:afterDelay needs to be performed on RunLoop thread - [self performSelectorOnMainThread:@selector(reschedule) withObject:nil waitUntilDone:NO]; +// Cached back buffers will be released after kIdleDelay if there is no activity. +static const double kIdleDelay = 1.0; - @synchronized(self) { - _frameInProgress = NO; - } +@interface FlutterBackBufferCache () { + NSMutableArray* _surfaces; } -- (void)reschedule { - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil]; - [self performSelector:@selector(onIdle) withObject:nil afterDelay:kIdleDelay]; +@end + +@implementation FlutterBackBufferCache + +- (instancetype)init { + if (self = [super init]) { + self->_surfaces = [[NSMutableArray alloc] init]; + } + return self; } -- (void)onIdle { +- (nullable FlutterSurface*)removeSurfaceForSize:(CGSize)size { @synchronized(self) { - if (!_frameInProgress) { - // Release the back buffer and notify delegate. The buffer will be restored - // on demand in ensureBackBuffer - _ioSurfaces[kFlutterSurfaceManagerBackBuffer] = nil; - _textures[kFlutterSurfaceManagerBackBuffer] = nil; + for (FlutterSurface* surface in _surfaces) { + if (CGSizeEqualToSize(surface.size, size)) { + // By default ARC doesn't retain enumeration iteration variables. + FlutterSurface* res = surface; + [_surfaces removeObject:surface]; + return res; + } } + return nil; } } -- (void)ensureBackBuffer { +- (void)replaceSurfaces:(nonnull NSArray*)surfaces { @synchronized(self) { - _frameInProgress = YES; - if (_ioSurfaces[kFlutterSurfaceManagerBackBuffer] == nil) { - // Restore previously released backbuffer - _ioSurfaces[kFlutterSurfaceManagerBackBuffer] = [[FlutterIOSurfaceHolder alloc] init]; - [_ioSurfaces[kFlutterSurfaceManagerBackBuffer] recreateIOSurfaceWithSize:_surfaceSize]; - _textures[kFlutterSurfaceManagerBackBuffer] = - [self createTextureForSurface:_ioSurfaces[kFlutterSurfaceManagerBackBuffer] - size:_surfaceSize]; - } - }; - [self performSelectorOnMainThread:@selector(cancelIdle) withObject:nil waitUntilDone:NO]; + [_surfaces removeAllObjects]; + [_surfaces addObjectsFromArray:surfaces]; + } + + // performSelector:withObject:afterDelay needs to be performed on RunLoop thread + [self performSelectorOnMainThread:@selector(reschedule) withObject:nil waitUntilDone:NO]; } -- (void)cancelIdle { - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil]; +- (NSUInteger)count { + @synchronized(self) { + return _surfaces.count; + } +} + +- (void)onIdle { + @synchronized(self) { + [_surfaces removeAllObjects]; + } } -- (nonnull FlutterRenderBackingStore*)renderBuffer { - [self ensureBackBuffer]; - id texture = _textures[kFlutterSurfaceManagerBackBuffer]; - return [[FlutterRenderBackingStore alloc] initWithTexture:texture]; +- (void)reschedule { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil]; + [self performSelector:@selector(onIdle) withObject:nil afterDelay:kIdleDelay]; } -- (id)createTextureForSurface:(FlutterIOSurfaceHolder*)surface size:(CGSize)size { - MTLTextureDescriptor* textureDescriptor = - [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm - width:size.width - height:size.height - mipmapped:NO]; - textureDescriptor.usage = - MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget | MTLTextureUsageShaderWrite; - // plane = 0 for BGRA. - return [_device newTextureWithDescriptor:textureDescriptor iosurface:[surface ioSurface] plane:0]; +- (void)dealloc { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil]; } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm index b9e1742abd547..058913ae2e6f8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManagerTest.mm @@ -3,14 +3,18 @@ // found in the LICENSE file. #import +#import #import +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" + #include "flutter/testing/testing.h" #include "gtest/gtest.h" -@interface TestView : NSView +@interface TestView : NSView +@property(readwrite, nonatomic) CGSize presentedFrameSize; - (nonnull instancetype)init; @end @@ -25,38 +29,172 @@ - (instancetype)init { return self; } +- (void)onPresent:(CGSize)frameSize withBlock:(nonnull dispatch_block_t)block { + self.presentedFrameSize = frameSize; + block(); +} + @end namespace flutter::testing { -static FlutterSurfaceManager* CreateSurfaceManager() { +static FlutterSurfaceManager* CreateSurfaceManager(TestView* testView) { id device = MTLCreateSystemDefaultDevice(); id commandQueue = [device newCommandQueue]; - TestView* metalView = [[TestView alloc] init]; - CALayer* layer = reinterpret_cast(metalView.layer); + CALayer* layer = reinterpret_cast(testView.layer); return [[FlutterSurfaceManager alloc] initWithDevice:device commandQueue:commandQueue - layer:layer]; -} - -TEST(FlutterSurfaceManager, EnsureSizeUpdatesSize) { - FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(); - CGSize size = CGSizeMake(100, 50); - [surfaceManager ensureSurfaceSize:size]; - id texture = [surfaceManager renderBuffer].texture; - CGSize textureSize = CGSizeMake(texture.width, texture.height); - ASSERT_TRUE(CGSizeEqualToSize(size, textureSize)); -} - -TEST(FlutterSurfaceManager, EnsureSizeUpdatesSizeForBackBuffer) { - FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(); - CGSize size = CGSizeMake(100, 50); - [surfaceManager ensureSurfaceSize:size]; - [surfaceManager renderBuffer]; // make sure we have back buffer - [surfaceManager swapBuffers]; - id texture = [surfaceManager renderBuffer].texture; - CGSize textureSize = CGSizeMake(texture.width, texture.height); - ASSERT_TRUE(CGSizeEqualToSize(size, textureSize)); + layer:layer + delegate:testView]; +} + +static FlutterSurfacePresentInfo* CreatePresentInfo(FlutterSurface* surface, + CGPoint offset = CGPointZero, + size_t index = 0) { + FlutterSurfacePresentInfo* res = [[FlutterSurfacePresentInfo alloc] init]; + res.surface = surface; + res.offset = offset; + res.zIndex = index; + return res; +} + +TEST(FlutterSurfaceManager, MetalTextureSizeMatchesSurfaceSize) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView); + + // Get back buffer, lookup should work for borrowed surfaces util present. + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + auto texture = surface.asFlutterMetalTexture; + id metalTexture = (__bridge id)texture.texture; + EXPECT_EQ(metalTexture.width, 100ul); + EXPECT_EQ(metalTexture.height, 50ul); + texture.destruction_callback(texture.user_data); +} + +TEST(FlutterSurfaceManager, TestSurfaceLookupFromTexture) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView); + + // Get back buffer, lookup should work for borrowed surfaces util present. + auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)]; + + // SurfaceManager should keep texture alive while borrowed. + auto texture = surface.asFlutterMetalTexture; + texture.destruction_callback(texture.user_data); + + FlutterMetalTexture dummyTexture{.texture_id = 1, .texture = nullptr, .user_data = nullptr}; + auto surface1 = [FlutterSurface fromFlutterMetalTexture:&dummyTexture]; + EXPECT_EQ(surface1, nil); + + auto surface2 = [FlutterSurface fromFlutterMetalTexture:&texture]; + EXPECT_EQ(surface2, surface); +} + +TEST(FlutterSurfaceManager, BackBufferCacheDoesNotLeak) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView); + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); + + auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)]; + [surfaceManager present:@[ CreatePresentInfo(surface1) ] notify:nil]; + + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); + + auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(110, 110)]; + [surfaceManager present:@[ CreatePresentInfo(surface2) ] notify:nil]; + + EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul); + + auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(120, 120)]; + [surfaceManager present:@[ CreatePresentInfo(surface3) ] notify:nil]; + + // Cache should be cleaned during present and only contain the last visible + // surface(s). + EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul); + auto surfaceFromCache = [surfaceManager surfaceForSize:CGSizeMake(110, 110)]; + EXPECT_EQ(surfaceFromCache, surface2); + + [surfaceManager present:@[] notify:nil]; + EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul); + + [surfaceManager present:@[] notify:nil]; + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); +} + +TEST(FlutterSurfaceManager, SurfacesAreRecycled) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView); + + EXPECT_EQ(surfaceManager.frontSurfaces.count, 0ul); + + // Get first surface and present it. + + auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)]; + EXPECT_TRUE(CGSizeEqualToSize(surface1.size, CGSizeMake(100, 100))); + + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); + EXPECT_EQ(surfaceManager.frontSurfaces.count, 0ul); + + [surfaceManager present:@[ CreatePresentInfo(surface1) ] notify:nil]; + + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); + EXPECT_EQ(surfaceManager.frontSurfaces.count, 1ul); + EXPECT_EQ(testView.layer.sublayers.count, 1ul); + + // Get second surface and present it. + + auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)]; + EXPECT_TRUE(CGSizeEqualToSize(surface2.size, CGSizeMake(100, 100))); + + EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul); + + [surfaceManager present:@[ CreatePresentInfo(surface2) ] notify:nil]; + + // Check that current front surface returns to cache. + EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul); + EXPECT_EQ(surfaceManager.frontSurfaces.count, 1ul); + EXPECT_EQ(testView.layer.sublayers.count, 1ull); + + // Check that surface is properly reused. + auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)]; + EXPECT_EQ(surface3, surface1); +} + +TEST(FlutterSurfaceManager, LayerManagement) { + TestView* testView = [[TestView alloc] init]; + FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView); + + 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]; + + 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:@[ + CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1), + CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2) + ] + notify:nil]; + + EXPECT_EQ(testView.layer.sublayers.count, 2ul); + EXPECT_EQ([testView.layer.sublayers objectAtIndex:0].zPosition, 1.0); + EXPECT_EQ([testView.layer.sublayers objectAtIndex:1].zPosition, 2.0); + EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 70))); + + auto surface3_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)]; + [surfaceManager present:@[ CreatePresentInfo(surface3_1, CGPointMake(20, 10)) ] 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]; + EXPECT_EQ(testView.layer.sublayers.count, 0ul); + EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(0, 0))); } } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h new file mode 100644 index 0000000000000..8eca9611f0f48 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h @@ -0,0 +1,36 @@ +#import + +/** + * Takes care of synchronization between raster and platform thread. + */ +@interface FlutterThreadSynchronizer : NSObject + +/** + * Blocks current thread until there is frame available. + * Used in FlutterEngineTest. + */ +- (void)blockUntilFrameAvailable; + +/** + * Called from platform thread. Blocks until commit with given size (or empty) + * is requested. + */ +- (void)beginResize:(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)performCommit:(CGSize)size notify:(nonnull dispatch_block_t)notify; + +/** + * Called when shutting down. Unblocks everything and prevents any further synchronization. + */ +- (void)shutdown; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm new file mode 100644 index 0000000000000..10f9ae53e5cce --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.mm @@ -0,0 +1,103 @@ +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h" +#import "fml/synchronization/waitable_event.h" + +#import +#include + +@interface FlutterThreadSynchronizer () { + std::mutex _mutex; + BOOL _shuttingDown; + CGSize _contentSize; + std::vector _scheduledBlocks; + + BOOL _beginResizeWaiting; + + // Used to block [beginResize:]. + std::condition_variable _condBlockBeginResize; +} + +@end + +@implementation FlutterThreadSynchronizer + +- (void)drain { + assert([NSThread isMainThread]); + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + for (dispatch_block_t block : _scheduledBlocks) { + block(); + } + [CATransaction commit]; + _scheduledBlocks.clear(); +} + +- (void)blockUntilFrameAvailable { + std::unique_lock lock(_mutex); + + _beginResizeWaiting = YES; + + while (CGSizeEqualToSize(_contentSize, CGSizeZero) && !_shuttingDown) { + _condBlockBeginResize.wait(lock); + [self drain]; + } + + _beginResizeWaiting = NO; +} + +- (void)beginResize:(CGSize)size notify:(nonnull dispatch_block_t)notify { + std::unique_lock lock(_mutex); + + if (CGSizeEqualToSize(_contentSize, CGSizeZero) || _shuttingDown) { + // No blocking until framework produces at least one frame + notify(); + return; + } + + [self drain]; + + notify(); + + _contentSize = CGSizeMake(-1, -1); + + _beginResizeWaiting = YES; + + while (!CGSizeEqualToSize(_contentSize, size) && // + !CGSizeEqualToSize(_contentSize, CGSizeZero) && !_shuttingDown) { + _condBlockBeginResize.wait(lock); + [self drain]; + } + + _beginResizeWaiting = NO; +} + +- (void)performCommit:(CGSize)size notify:(nonnull dispatch_block_t)notify { + fml::AutoResetWaitableEvent event; + { + std::unique_lock lock(_mutex); + fml::AutoResetWaitableEvent& e = event; + _scheduledBlocks.push_back(^{ + notify(); + _contentSize = size; + e.Signal(); + }); + if (_beginResizeWaiting) { + _condBlockBeginResize.notify_all(); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + std::unique_lock lock(_mutex); + [self drain]; + }); + } + } + event.Wait(); +} + +- (void)shutdown { + std::unique_lock lock(_mutex); + _shuttingDown = YES; + _condBlockBeginResize.notify_all(); + [self drain]; +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index 2da2f48f34963..ba70833b46b76 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -3,9 +3,11 @@ // found in the LICENSE file. #import -#include -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterResizableBackingStoreProvider.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterThreadSynchronizer.h" + +#include /** * The view ID for APIs that don't support multi-view. @@ -49,21 +51,10 @@ constexpr uint64_t kFlutterDefaultViewId = 0; - (nonnull instancetype)init NS_UNAVAILABLE; /** - * Flushes the graphics context and flips the surfaces. Expected to be called on raster thread. - */ -- (void)present; - -/** - * Called when there is no Flutter content available to render. This must be passed to resize - * synchronizer. + * Returns SurfaceManager for this view. SurfaceManager is responsible for + * providing and presenting render surfaces. */ -- (void)presentWithoutContent; - -/** - * Ensures that a backing store with requested size exists and returns the descriptor. Expected to - * be called on raster thread. - */ -- (nonnull FlutterRenderBackingStore*)backingStoreForSize:(CGSize)size; +@property(readonly, nonatomic, nonnull) FlutterSurfaceManager* surfaceManager; /** * Must be called when shutting down. Unblocks raster thread and prevents any further @@ -81,3 +72,13 @@ constexpr uint64_t kFlutterDefaultViewId = 0; - (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 8a92b9f5fbb59..9991918153dfa 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -4,15 +4,15 @@ #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 () { +@interface FlutterView () { __weak id _reshapeListener; - FlutterResizeSynchronizer* _resizeSynchronizer; - FlutterResizableBackingStoreProvider* _resizableBackingStoreProvider; + FlutterThreadSynchronizer* _threadSynchronizer; + FlutterSurfaceManager* _surfaceManager; } @end @@ -28,34 +28,30 @@ - (instancetype)initWithMTLDevice:(id)device [self setBackgroundColor:[NSColor blackColor]]; [self setLayerContentsRedrawPolicy:NSViewLayerContentsRedrawDuringViewResize]; _reshapeListener = reshapeListener; - _resizableBackingStoreProvider = - [[FlutterResizableBackingStoreProvider alloc] initWithDevice:device - commandQueue:commandQueue - layer:self.layer]; - _resizeSynchronizer = - [[FlutterResizeSynchronizer alloc] initWithDelegate:_resizableBackingStoreProvider]; + _threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; + _surfaceManager = [[FlutterSurfaceManager alloc] initWithDevice:device + commandQueue:commandQueue + layer:self.layer + delegate:self]; } return self; } -- (FlutterRenderBackingStore*)backingStoreForSize:(CGSize)size { - if ([_resizeSynchronizer shouldEnsureSurfaceForSize:size]) { - [_resizableBackingStoreProvider onBackingStoreResized:size]; - } - return [_resizableBackingStoreProvider backingStore]; +- (void)onPresent:(CGSize)frameSize withBlock:(dispatch_block_t)block { + [_threadSynchronizer performCommit:frameSize notify:block]; } -- (void)present { - [_resizeSynchronizer requestCommit]; +- (FlutterSurfaceManager*)surfaceManager { + return _surfaceManager; } -- (void)presentWithoutContent { - [_resizeSynchronizer noFlutterContent]; +- (FlutterThreadSynchronizer*)threadSynchronizer { + return _threadSynchronizer; } - (void)reshaped { CGSize scaledSize = [self convertSizeToBacking:self.bounds.size]; - [_resizeSynchronizer beginResize:scaledSize + [_threadSynchronizer beginResize:scaledSize notify:^{ [_reshapeListener viewDidReshape:self]; }]; @@ -111,7 +107,7 @@ - (void)viewDidChangeBackingProperties { } - (void)shutdown { - [_resizeSynchronizer shutdown]; + [_threadSynchronizer shutdown]; } #pragma mark - NSAccessibility overrides