From d97ebaa00f096544d70732d56e39b9249725b1f8 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Fri, 11 Sep 2020 19:28:57 +0200 Subject: [PATCH] Smooth resizing for macOS embedder --- shell/platform/darwin/macos/BUILD.gn | 6 + .../macos/framework/Source/FlutterEngine.mm | 20 +-- .../Source/FlutterResizeSynchronizer.h | 34 +++++ .../Source/FlutterResizeSynchronizer.mm | 97 ++++++++++++++ .../framework/Source/FlutterSurfaceManager.h | 13 ++ .../framework/Source/FlutterSurfaceManager.mm | 119 ++++++++++++++++++ .../macos/framework/Source/FlutterView.h | 8 +- .../macos/framework/Source/FlutterView.mm | 82 ++++++++++-- 8 files changed, 363 insertions(+), 16 deletions(-) create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h create mode 100644 shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 58a9e66bcb180..6096b134abf6e 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -54,6 +54,10 @@ source_set("flutter_framework_source") { "framework/Source/FlutterExternalTextureGL.mm", "framework/Source/FlutterMouseCursorPlugin.h", "framework/Source/FlutterMouseCursorPlugin.mm", + "framework/Source/FlutterResizeSynchronizer.h", + "framework/Source/FlutterResizeSynchronizer.mm", + "framework/Source/FlutterSurfaceManager.h", + "framework/Source/FlutterSurfaceManager.mm", "framework/Source/FlutterTextInputModel.h", "framework/Source/FlutterTextInputModel.mm", "framework/Source/FlutterTextInputPlugin.h", @@ -81,6 +85,8 @@ source_set("flutter_framework_source") { libs = [ "Cocoa.framework", "CoreVideo.framework", + "IOSurface.framework", + "QuartzCore.framework", ] } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 99125f651744e..3d98704fa24ed 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -139,9 +139,9 @@ static bool OnPresent(FlutterEngine* engine) { return [engine engineCallbackOnPresent]; } -static uint32_t OnFBO(FlutterEngine* engine) { - // There is currently no case where a different FBO is used, so no need to forward. - return 0; +static uint32_t OnFBO(FlutterEngine* engine, const FlutterFrameInfo* info) { + CGSize size = CGSizeMake(info->size.width, info->size.height); + return [engine.viewController.flutterView getFrameBufferIdForSize:size]; } static bool OnMakeResourceCurrent(FlutterEngine* engine) { @@ -230,7 +230,8 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { .open_gl.make_current = (BoolCallback)OnMakeCurrent, .open_gl.clear_current = (BoolCallback)OnClearCurrent, .open_gl.present = (BoolCallback)OnPresent, - .open_gl.fbo_callback = (UIntCallback)OnFBO, + .open_gl.fbo_with_frame_info_callback = (UIntFrameInfoCallback)OnFBO, + .open_gl.fbo_reset_after_present = true, .open_gl.make_resource_current = (BoolCallback)OnMakeResourceCurrent, .open_gl.gl_external_texture_frame_callback = (TextureFrameCallback)OnAcquireExternalTexture, }; @@ -287,8 +288,8 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { } [self sendUserLocales]; - [self updateWindowMetrics]; [self updateDisplayConfig]; + [self.viewController.flutterView start]; return YES; } @@ -299,7 +300,9 @@ - (void)setViewController:(FlutterViewController*)controller { [self shutDownEngine]; _resourceContext = nil; } - [self updateWindowMetrics]; + if (_engine) { + [self.viewController.flutterView start]; + } } - (id)binaryMessenger { @@ -317,7 +320,7 @@ - (BOOL)running { - (NSOpenGLContext*)resourceContext { if (!_resourceContext) { NSOpenGLPixelFormatAttribute attributes[] = { - NSOpenGLPFAColorSize, 24, NSOpenGLPFAAlphaSize, 8, NSOpenGLPFADoubleBuffer, 0, + NSOpenGLPFAColorSize, 24, NSOpenGLPFAAlphaSize, 8, 0, }; NSOpenGLPixelFormat* pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes]; _resourceContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil]; @@ -350,6 +353,7 @@ - (void)updateDisplayConfig { CVDisplayLinkRelease(displayLinkRef); } +// Must be driven by FlutterView (i.e. [FlutterView start]) - (void)updateWindowMetrics { if (!_engine) { return; @@ -413,7 +417,7 @@ - (bool)engineCallbackOnPresent { if (!_mainOpenGLContext) { return false; } - [_mainOpenGLContext flushBuffer]; + [self.viewController.flutterView present]; return true; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h new file mode 100644 index 0000000000000..30a68a47b35eb --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h @@ -0,0 +1,34 @@ +#import + +@class FlutterResizeSynchronizer; + +@protocol FlutterResizeSynchronizerDelegate + +// Invoked on platform thread; Delegate should flush OpenGL context and +// flip the surfaces +- (void)resizeSynchronizerCommit:(FlutterResizeSynchronizer*)synchronizer; + +@end + +// Encapsulates the logic for blocking platform thread during window resize +@interface FlutterResizeSynchronizer : NSObject + +- (instancetype)initWithDelegate:(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; +- (void)beginResize:(CGSize)size notify:(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; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm new file mode 100644 index 0000000000000..b689cfe28ad7b --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm @@ -0,0 +1,97 @@ +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.h" + +#import + +@interface FlutterResizeSynchronizer () { + uint32_t cookie; // counter to detect stale callbacks + std::mutex mutex; + std::condition_variable condRun; + std::condition_variable condBlock; + bool accepting; + bool waiting; + bool pendingCommit; + CGSize newSize; + __weak id delegate; +} +@end + +@implementation FlutterResizeSynchronizer + +- (instancetype)initWithDelegate:(id)delegate_ { + if (self = [super init]) { + accepting = true; + delegate = delegate_; + } + return self; +} + +- (void)beginResize:(CGSize)size notify:(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 + condRun.notify_all(); + + // let the engine send resize notification + notify(); + + newSize = size; + + waiting = true; + + condBlock.wait(lock); + + if (pendingCommit) { + [delegate resizeSynchronizerCommit:self]; + pendingCommit = false; + condRun.notify_all(); + } + + waiting = false; +} + +- (bool)shouldEnsureSurfaceForSize:(CGSize)size { + std::unique_lock lock(mutex); + if (!accepting) { + if (CGSizeEqualToSize(newSize, size)) { + accepting = true; + } + } + return accepting; +} + +- (void)requestCommit { + std::unique_lock lock(mutex); + if (!accepting) { + return; + } + + if (waiting) { // BeginResize is in progress, interrupt it and schedule commit call + pendingCommit = true; + condBlock.notify_all(); + condRun.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(), [self, cookie_ = cookie] { + std::unique_lock lock(mutex); + if (cookie_ == cookie) { + if (delegate) { + [delegate resizeSynchronizerCommit:self]; + } + condRun.notify_all(); + } + }); + condRun.wait(lock); + } +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h new file mode 100644 index 0000000000000..470f4f9400fda --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h @@ -0,0 +1,13 @@ +#import + +// Manages the IOSurfaces for FlutterView +@interface FlutterSurfaceManager : NSObject + +- (instancetype)initWithLayer:(CALayer*)layer openGLContext:(NSOpenGLContext*)opengLContext; + +- (void)ensureSurfaceSize:(CGSize)size; +- (void)swapBuffers; + +- (uint32_t)glFrameBufferId; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm new file mode 100644 index 0000000000000..657b84d837eb4 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.mm @@ -0,0 +1,119 @@ +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" + +#include + +enum { + kFront = 0, + kBack = 1, + kBufferCount, +}; + +@interface FlutterSurfaceManager () { + CGSize surfaceSize; + CALayer* layer; + NSOpenGLContext* openGLContext; + uint32_t _frameBufferId[kBufferCount]; + uint32_t _backingTexture[kBufferCount]; + IOSurfaceRef _ioSurface[kBufferCount]; +} +@end + +@implementation FlutterSurfaceManager + +- (instancetype)initWithLayer:(CALayer*)layer_ openGLContext:(NSOpenGLContext*)opengLContext_ { + if (self = [super init]) { + layer = layer_; + openGLContext = opengLContext_; + + NSOpenGLContext* prev = [NSOpenGLContext currentContext]; + [openGLContext makeCurrentContext]; + glGenFramebuffers(2, _frameBufferId); + glGenTextures(2, _backingTexture); + + glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferId[0]); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, _backingTexture[0]); + 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); + + glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferId[1]); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, _backingTexture[1]); + 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); + + if (prev) { + [prev makeCurrentContext]; + } else { + [NSOpenGLContext clearCurrentContext]; + } + } + return self; +} + +- (void)ensureSurfaceSize:(CGSize)size { + if (CGSizeEqualToSize(size, surfaceSize)) { + return; + } + surfaceSize = size; + NSOpenGLContext* prev = [NSOpenGLContext currentContext]; + [openGLContext makeCurrentContext]; + + for (int i = 0; i < 2; ++i) { + if (_ioSurface[i]) { + CFRelease(_ioSurface[i]); + } + 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[i] = IOSurfaceCreate((CFDictionaryRef)options); + + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, _backingTexture[i]); + + CGLTexImageIOSurface2D(CGLGetCurrentContext(), GL_TEXTURE_RECTANGLE_ARB, GL_RGBA, + int(size.width), int(size.height), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, + _ioSurface[i], 0 /* plane */); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0); + + glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferId[i]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, + _backingTexture[i], 0); + } + if (prev) { + [prev makeCurrentContext]; + } else { + [NSOpenGLContext clearCurrentContext]; + } +} + +- (void)swapBuffers { + [layer setContents:(__bridge id)_ioSurface[kBack]]; + std::swap(_ioSurface[kBack], _ioSurface[kFront]); + std::swap(_frameBufferId[kBack], _frameBufferId[kFront]); + std::swap(_backingTexture[kBack], _backingTexture[kFront]); +} + +- (uint32_t)glFrameBufferId { + return _frameBufferId[kBack]; +} + +- (void)dealloc { + for (int i = 0; i < kBufferCount; ++i) { + if (_ioSurface[i]) { + CFRelease(_ioSurface[i]); + } + } +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index e8c7ec3bd3d93..4123d1a5fde30 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -18,7 +18,9 @@ * 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; - (nullable instancetype)initWithFrame:(NSRect)frame shareContext:(nonnull NSOpenGLContext*)shareContext @@ -35,4 +37,8 @@ - (nullable instancetype)initWithCoder:(nonnull NSCoder*)coder NS_UNAVAILABLE; - (nonnull instancetype)init NS_UNAVAILABLE; +- (void)start; +- (void)present; +- (int)getFrameBufferIdForSize:(CGSize)size; + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.mm b/shell/platform/darwin/macos/framework/Source/FlutterView.mm index 1b46ac600ed9e..9db041e37eacd 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.mm @@ -3,11 +3,24 @@ // found in the LICENSE file. #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" -@implementation FlutterView { +#import +#import + +@interface FlutterView () { __weak id _reshapeListener; + FlutterResizeSynchronizer* resizeSynchronizer; + FlutterSurfaceManager* surfaceManager; + BOOL active; + CALayer* contentLayer; } +@end + +@implementation FlutterView + - (instancetype)initWithShareContext:(NSOpenGLContext*)shareContext reshapeListener:(id)reshapeListener { return [self initWithFrame:NSZeroRect shareContext:shareContext reshapeListener:reshapeListener]; @@ -20,14 +33,73 @@ - (instancetype)initWithFrame:(NSRect)frame if (self) { self.openGLContext = [[NSOpenGLContext alloc] initWithFormat:shareContext.pixelFormat shareContext:shareContext]; + + [self setWantsLayer:YES]; + + // Layer for content (which will be set by surfaceManager). This is separate from + // self.layer because it needs to be flipped vertically (using layer.sublayerTransform) + contentLayer = [[CALayer alloc] init]; + [self.layer addSublayer:contentLayer]; + + resizeSynchronizer = [[FlutterResizeSynchronizer alloc] initWithDelegate:self]; + surfaceManager = [[FlutterSurfaceManager alloc] initWithLayer:contentLayer + openGLContext:self.openGLContext]; + _reshapeListener = reshapeListener; - self.wantsBestResolutionOpenGLSurface = YES; } return self; } +- (void)resizeSynchronizerCommit:(FlutterResizeSynchronizer*)synchronizer { + [self.openGLContext makeCurrentContext]; + glFlush(); + [NSOpenGLContext clearCurrentContext]; + + [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; + + [surfaceManager swapBuffers]; + + [CATransaction commit]; +} + +- (int)getFrameBufferIdForSize:(CGSize)size { + if ([resizeSynchronizer shouldEnsureSurfaceForSize:size]) { + [surfaceManager ensureSurfaceSize:size]; + } + return [surfaceManager glFrameBufferId]; +} + +- (void)present { + [resizeSynchronizer requestCommit]; +} + +- (void)start { + active = YES; + [self reshaped]; +} + +- (void)reshaped { + if (active) { + CGSize scaledSize = [self convertSizeToBacking:self.bounds.size]; + [resizeSynchronizer beginResize:scaledSize + notify:^{ + [_reshapeListener viewDidReshape:self]; + }]; + } +} + #pragma mark - NSView overrides +- (void)setFrameSize:(NSSize)newSize { + [super setFrameSize:newSize]; + [self reshaped]; +} + /** * Declares that the view uses a flipped coordinate system, consistent with Flutter conventions. */ @@ -39,17 +111,13 @@ - (BOOL)isOpaque { return YES; } -- (void)reshape { - [super reshape]; - [_reshapeListener viewDidReshape:self]; -} - - (BOOL)acceptsFirstResponder { return YES; } - (void)viewDidChangeBackingProperties { [super viewDidChangeBackingProperties]; + // Force redraw [_reshapeListener viewDidReshape:self]; }