diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index bacb6af24a1b2..573349f39a2c2 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -67,6 +67,7 @@ source_set("flutter_framework_source") { sources += _flutter_framework_headers deps = [ + "//flutter/fml:fml", "//flutter/shell/platform/darwin/common:framework_shared", "//flutter/shell/platform/embedder:embedder_as_internal_library", ] @@ -79,7 +80,9 @@ source_set("flutter_framework_source") { libs = [ "Cocoa.framework", + "QuartzCore.framework", "CoreVideo.framework", + "IOSurface.framework", ] } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 36b24f3424ddc..32f922979596c 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -139,8 +139,7 @@ static bool OnPresent(FlutterEngine* engine) { } static uint32_t OnFBO(FlutterEngine* engine) { - // There is currently no case where a different FBO is used, so no need to forward. - return 0; + return engine.viewController.flutterView.frameBufferId; } static bool OnMakeResourceCurrent(FlutterEngine* engine) { @@ -262,7 +261,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { const FlutterCustomTaskRunners custom_task_runners = { .struct_size = sizeof(FlutterCustomTaskRunners), .platform_task_runner = &cocoa_task_runner_description, - .render_task_runner = &cocoa_task_runner_description, + .render_task_runner = nullptr, }; flutterArguments.custom_task_runners = &custom_task_runners; @@ -280,7 +279,8 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { } [self sendUserLocales]; - [self updateWindowMetrics]; + + [self.viewController.flutterView start]; return YES; } @@ -291,7 +291,9 @@ - (void)setViewController:(FlutterViewController*)controller { [self shutDownEngine]; _resourceContext = nil; } - [self updateWindowMetrics]; + if (_engine) { + [self.viewController.flutterView start]; + } } - (id)binaryMessenger { @@ -317,6 +319,7 @@ - (NSOpenGLContext*)resourceContext { return _resourceContext; } +// Must be driven by FlutterView (i.e. [FlutterView start]) - (void)updateWindowMetrics { if (!_engine) { return; @@ -334,6 +337,18 @@ - (void)updateWindowMetrics { FlutterEngineSendWindowMetricsEvent(_engine, &event); } +- (void)scheduleOnRasterTread:(dispatch_block_t)block { + void* copy = Block_copy((__bridge void*)block); + FlutterEnginePostRenderThreadTask( + _engine, + [](void* cb) { + dispatch_block_t block = (__bridge dispatch_block_t)cb; + block(); + Block_release(block); + }, + copy); +} + - (void)sendPointerEvent:(const FlutterPointerEvent&)event { FlutterEngineSendPointerEvent(_engine, &event, 1); } @@ -380,7 +395,7 @@ - (bool)engineCallbackOnPresent { if (!_mainOpenGLContext) { return false; } - [_mainOpenGLContext flushBuffer]; + [self.viewController.flutterView present]; return true; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index 2a01586efa27e..6148243561494 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -26,6 +26,11 @@ */ - (void)updateWindowMetrics; +/** + * Schedules the block on rater thread. + */ +- (void)scheduleOnRasterTread:(nonnull dispatch_block_t)block; + /** * Dispatches the given pointer event data to engine. */ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index e8c7ec3bd3d93..b51357ce1b518 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -7,27 +7,31 @@ /** * Listener for view resizing. */ -@protocol FlutterViewReshapeListener +@protocol FlutterViewDelegate /** * Called when the view's backing store changes size. */ - (void)viewDidReshape:(nonnull NSView*)view; +- (void)scheduleOnRasterTread:(nonnull dispatch_block_t)block; + @end /** * View capable of acting as a rendering target and input source for the Flutter * engine. */ -@interface FlutterView : NSOpenGLView +@interface FlutterView : NSView + +@property(readwrite, nonatomic, nonnull) NSOpenGLContext* openGLContext; +@property(readonly, nonatomic) uint32_t frameBufferId; - (nullable instancetype)initWithFrame:(NSRect)frame shareContext:(nonnull NSOpenGLContext*)shareContext - reshapeListener:(nonnull id)reshapeListener + delegate:(nonnull id)delegate NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithShareContext:(nonnull NSOpenGLContext*)shareContext - reshapeListener: - (nonnull id)reshapeListener; + delegate:(nonnull id)delegate; - (nullable instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(nullable NSOpenGLPixelFormat*)format NS_UNAVAILABLE; @@ -35,4 +39,7 @@ - (nullable instancetype)initWithCoder:(nonnull NSCoder*)coder NS_UNAVAILABLE; - (nonnull instancetype)init NS_UNAVAILABLE; +- (void)start; +- (void)present; + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/shell/platform/darwin/macos/framework/Source/FlutterView.mm index 1b46ac600ed9e..4e43db43a4adb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -3,31 +3,250 @@ // found in the LICENSE file. #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" +#include +#include +#import -@implementation FlutterView { - __weak id _reshapeListener; +#import +#import +#include "flutter/fml/memory/ref_counted.h" + +@protocol SynchronizerDelegate + +// Invoked on raster thread; Delegate should recreate IOSurface with given size +- (void)recreateSurfaceWithScaledSize:(CGSize)scaledSize; + +// Invoked on platform thread; Delegate should flush the OpenGL context and +// update the surface +- (void)commit; + +- (void)scheduleOnRasterThread:(dispatch_block_t)block; + +@end + +namespace { +class ResizeSynchronizer; +} + +@interface FlutterView () { + __weak id _delegate; + uint32_t _frameBufferId; + uint32_t _backingTexture; + IOSurfaceRef _ioSurface; + fml::RefPtr synchronizer; + BOOL active; + CALayer* contentLayer; } +@end + +@implementation FlutterView + +namespace { + +class ResizeSynchronizer : public fml::RefCountedThreadSafe { + public: + explicit ResizeSynchronizer(id delegate) : delegate_(delegate) {} + + // Begins resize event; Will hold the thread until Commit is called; + // While holding the thread event loop is being processed + void BeginResize(CGSize scaledSize, dispatch_block_t notify) { + std::unique_lock lock(mutex_); + if (!delegate_) { + return; + } + + ++cookie_; + + // from now on, ignore all incoming commits until the block below gets + // scheduled on raster thread + accepting_ = false; + + // let pending commits finish to unblock the raster thread + cond_run_.notify_all(); + + // let the engine send resize notification + notify(); + + auto self = fml::Ref(this); + + // after this block is executed we start accepting commits + [delegate_ scheduleOnRasterThread:[self, this, cookie = cookie_, scaledSize]() { + std::unique_lock lock(mutex_); + id delegate = delegate_; + if (cookie_ == cookie && delegate) { + accepting_ = true; + [delegate recreateSurfaceWithScaledSize:scaledSize]; + } + }]; + + waiting_ = true; + + cond_block_.wait(lock); + + if (pending_commit_) { + [delegate_ commit]; + pending_commit_ = false; + cond_run_.notify_all(); + } + + waiting_ = false; + } + + // Must be invoked on raster thread; Will synchronously invoke + // [delegate commit] on platform thread; + void Commit() { + std::unique_lock lock(mutex_); + if (!accepting_) { + return; + } + + if (waiting_) { // BeginResize is in progress, interrupt it and schedule commit call + pending_commit_ = true; + cond_block_.notify_all(); + cond_run_.wait(lock); + } else { + // No resize, schedule commit on platform thread and wait until either done + // or interrupted by incoming BeginResize + dispatch_async(dispatch_get_main_queue(), [this, cookie = cookie_] { + std::unique_lock lock(mutex_); + if (cookie_ == cookie) { + id delegate = delegate_; + if (delegate) { + [delegate commit]; + } + cond_run_.notify_all(); + } + }); + cond_run_.wait(lock); + } + } + + private: + uint32_t cookie_ = 0; // counter to detect stale callbacks + std::mutex mutex_; + std::condition_variable cond_run_; + std::condition_variable cond_block_; + bool accepting_ = true; + + bool waiting_ = false; + bool pending_commit_ = false; + + __weak id delegate_; +}; +} // namespace + - (instancetype)initWithShareContext:(NSOpenGLContext*)shareContext - reshapeListener:(id)reshapeListener { - return [self initWithFrame:NSZeroRect shareContext:shareContext reshapeListener:reshapeListener]; + delegate:(id)delegate { + return [self initWithFrame:NSZeroRect shareContext:shareContext delegate:delegate]; } - (instancetype)initWithFrame:(NSRect)frame shareContext:(NSOpenGLContext*)shareContext - reshapeListener:(id)reshapeListener { + delegate:(id)delegate { self = [super initWithFrame:frame]; if (self) { self.openGLContext = [[NSOpenGLContext alloc] initWithFormat:shareContext.pixelFormat shareContext:shareContext]; - _reshapeListener = reshapeListener; - self.wantsBestResolutionOpenGLSurface = YES; + + [self setWantsLayer:YES]; + + // covers entire view + contentLayer = [[CALayer alloc] init]; + [self.layer addSublayer:contentLayer]; + + synchronizer = fml::MakeRefCounted(self); + + [self.openGLContext makeCurrentContext]; + + glGenFramebuffers(1, &_frameBufferId); + glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferId); + + glGenTextures(1, &_backingTexture); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, _backingTexture); + glTexParameterf(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); + + _delegate = delegate; } return self; } +- (void)recreateSurfaceWithScaledSize:(CGSize)scaledSize { + if (_ioSurface) { + CFRelease(_ioSurface); + } + + [self.openGLContext makeCurrentContext]; + + unsigned pixelFormat = 'BGRA'; + unsigned bytesPerElement = 4; + + size_t bytesPerRow = + IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, scaledSize.width * bytesPerElement); + size_t totalBytes = IOSurfaceAlignProperty(kIOSurfaceAllocSize, scaledSize.height * bytesPerRow); + NSDictionary* options = @{ + (id)kIOSurfaceWidth : @(scaledSize.width), + (id)kIOSurfaceHeight : @(scaledSize.height), + (id)kIOSurfacePixelFormat : @(pixelFormat), + (id)kIOSurfaceBytesPerElement : @(bytesPerElement), + (id)kIOSurfaceBytesPerRow : @(bytesPerRow), + (id)kIOSurfaceAllocSize : @(totalBytes), + }; + _ioSurface = IOSurfaceCreate((CFDictionaryRef)options); + + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, _backingTexture); + + CGLTexImageIOSurface2D(CGLGetCurrentContext(), GL_TEXTURE_RECTANGLE_ARB, GL_RGBA, + int(scaledSize.width), int(scaledSize.height), GL_BGRA, + GL_UNSIGNED_INT_8_8_8_8_REV, _ioSurface, 0 /* plane */); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); + + glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferId); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, + _backingTexture, 0); +} + +- (void)reshaped { + if (active) { + CGSize scaledSize = [self convertRectToBacking:self.bounds].size; + synchronizer->BeginResize(scaledSize, ^{ + [_delegate viewDidReshape:self]; + }); + } +} + #pragma mark - NSView overrides +- (void)setFrameSize:(NSSize)newSize { + [super setFrameSize:newSize]; + [self reshaped]; +} + +- (void)commit { + [self.openGLContext flushBuffer]; + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + self.layer.frame = self.bounds; + self.layer.sublayerTransform = CATransform3DTranslate(CATransform3DMakeScale(1, -1, 1), 0, + -self.layer.bounds.size.height, 0); + contentLayer.frame = self.layer.bounds; + + [contentLayer setContents:nil]; + [contentLayer setContents:(__bridge id)_ioSurface]; + [CATransaction commit]; +} + +- (void)scheduleOnRasterThread:(dispatch_block_t)block { + [_delegate scheduleOnRasterTread:block]; +} + +- (void)present { + synchronizer->Commit(); +} + /** * Declares that the view uses a flipped coordinate system, consistent with Flutter conventions. */ @@ -39,9 +258,10 @@ - (BOOL)isOpaque { return YES; } -- (void)reshape { - [super reshape]; - [_reshapeListener viewDidReshape:self]; +- (void)dealloc { + if (_ioSurface) { + CFRelease(_ioSurface); + } } - (BOOL)acceptsFirstResponder { @@ -50,7 +270,12 @@ - (BOOL)acceptsFirstResponder { - (void)viewDidChangeBackingProperties { [super viewDidChangeBackingProperties]; - [_reshapeListener viewDidReshape:self]; + [self reshaped]; +} + +- (void)start { + active = YES; + [self reshaped]; } @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index b1a6055e28f70..6814d0a2dae18 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -80,7 +80,7 @@ void Reset() { /** * Private interface declaration for FlutterViewController. */ -@interface FlutterViewController () +@interface FlutterViewController () /** * A list of additional responders to keyboard events. Keybord events are forwarded to all of them. @@ -241,7 +241,7 @@ - (void)loadView { return; } FlutterView* flutterView = [[FlutterView alloc] initWithShareContext:resourceContext - reshapeListener:self]; + delegate:self]; self.view = flutterView; } @@ -534,7 +534,7 @@ - (void)setClipboardData:(NSDictionary*)data { } } -#pragma mark - FlutterViewReshapeListener +#pragma mark - FlutterViewDelegate /** * Responds to view reshape by notifying the engine of the change in dimensions. @@ -543,6 +543,10 @@ - (void)viewDidReshape:(NSView*)view { [_engine updateWindowMetrics]; } +- (void)scheduleOnRasterTread:(dispatch_block_t)block { + [_engine scheduleOnRasterTread:block]; +} + #pragma mark - FlutterPluginRegistry - (id)registrarForPlugin:(NSString*)pluginName {