| // 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. |
| |
| #include <IOSurface/IOSurfaceObjC.h> |
| #include <Metal/Metal.h> |
| #include <UIKit/UIKit.h> |
| |
| #include "flutter/fml/logging.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h" |
| |
| @interface DisplayLinkManager : NSObject |
| @property(class, nonatomic, readonly) BOOL maxRefreshRateEnabledOnIPhone; |
| + (double)displayRefreshRate; |
| @end |
| |
| @class FlutterTexture; |
| @class FlutterDrawable; |
| |
| extern CFTimeInterval display_link_target; |
| |
| @interface FlutterMetalLayer () { |
| id<MTLDevice> _preferredDevice; |
| CGSize _drawableSize; |
| |
| NSUInteger _nextDrawableId; |
| |
| NSMutableSet<FlutterTexture*>* _availableTextures; |
| NSUInteger _totalTextures; |
| |
| FlutterTexture* _front; |
| |
| // There must be a CADisplayLink scheduled *on main thread* otherwise |
| // core animation only updates layers 60 times a second. |
| CADisplayLink* _displayLink; |
| NSUInteger _displayLinkPauseCountdown; |
| |
| // Used to track whether the content was set during this display link. |
| // When unlocking phone the layer (main thread) display link and raster thread |
| // display link get out of sync for several seconds. Even worse, layer display |
| // link does not seem to reflect actual vsync. Forcing the layer link |
| // to max rate (instead range) temporarily seems to fix the issue. |
| BOOL _didSetContentsDuringThisDisplayLinkPeriod; |
| |
| // Whether layer displayLink is forced to max rate. |
| BOOL _displayLinkForcedMaxRate; |
| } |
| |
| - (void)presentTexture:(FlutterTexture*)texture; |
| - (void)returnTexture:(FlutterTexture*)texture; |
| |
| @end |
| |
| @interface FlutterTexture : NSObject { |
| id<MTLTexture> _texture; |
| IOSurface* _surface; |
| CFTimeInterval _presentedTime; |
| } |
| |
| @property(readonly, nonatomic) id<MTLTexture> texture; |
| @property(readonly, nonatomic) IOSurface* surface; |
| @property(readwrite, nonatomic) CFTimeInterval presentedTime; |
| @property(readwrite, atomic) BOOL waitingForCompletion; |
| |
| @end |
| |
| @implementation FlutterTexture |
| |
| @synthesize texture = _texture; |
| @synthesize surface = _surface; |
| @synthesize presentedTime = _presentedTime; |
| @synthesize waitingForCompletion; |
| |
| - (instancetype)initWithTexture:(id<MTLTexture>)texture surface:(IOSurface*)surface { |
| if (self = [super init]) { |
| _texture = texture; |
| _surface = surface; |
| } |
| return self; |
| } |
| |
| @end |
| |
| @interface FlutterDrawable : NSObject <FlutterMetalDrawable> { |
| FlutterTexture* _texture; |
| __weak FlutterMetalLayer* _layer; |
| NSUInteger _drawableId; |
| BOOL _presented; |
| } |
| |
| - (instancetype)initWithTexture:(FlutterTexture*)texture |
| layer:(FlutterMetalLayer*)layer |
| drawableId:(NSUInteger)drawableId; |
| |
| @end |
| |
| @implementation FlutterDrawable |
| |
| - (instancetype)initWithTexture:(FlutterTexture*)texture |
| layer:(FlutterMetalLayer*)layer |
| drawableId:(NSUInteger)drawableId { |
| if (self = [super init]) { |
| _texture = texture; |
| _layer = layer; |
| _drawableId = drawableId; |
| } |
| return self; |
| } |
| |
| - (id<MTLTexture>)texture { |
| return self->_texture.texture; |
| } |
| |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wunguarded-availability-new" |
| - (CAMetalLayer*)layer { |
| return (id)self->_layer; |
| } |
| #pragma clang diagnostic pop |
| |
| - (NSUInteger)drawableID { |
| return self->_drawableId; |
| } |
| |
| - (CFTimeInterval)presentedTime { |
| return 0; |
| } |
| |
| - (void)present { |
| [_layer presentTexture:self->_texture]; |
| self->_presented = YES; |
| } |
| |
| - (void)dealloc { |
| if (!_presented) { |
| [_layer returnTexture:self->_texture]; |
| } |
| } |
| |
| - (void)addPresentedHandler:(nonnull MTLDrawablePresentedHandler)block { |
| FML_LOG(WARNING) << "FlutterMetalLayer drawable does not implement addPresentedHandler:"; |
| } |
| |
| - (void)presentAtTime:(CFTimeInterval)presentationTime { |
| FML_LOG(WARNING) << "FlutterMetalLayer drawable does not implement presentAtTime:"; |
| } |
| |
| - (void)presentAfterMinimumDuration:(CFTimeInterval)duration { |
| FML_LOG(WARNING) << "FlutterMetalLayer drawable does not implement presentAfterMinimumDuration:"; |
| } |
| |
| - (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer { |
| FlutterTexture* texture = _texture; |
| texture.waitingForCompletion = YES; |
| [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) { |
| texture.waitingForCompletion = NO; |
| }]; |
| } |
| |
| @end |
| |
| @implementation FlutterMetalLayer |
| |
| @synthesize preferredDevice = _preferredDevice; |
| @synthesize device = _device; |
| @synthesize pixelFormat = _pixelFormat; |
| @synthesize framebufferOnly = _framebufferOnly; |
| @synthesize colorspace = _colorspace; |
| @synthesize wantsExtendedDynamicRangeContent = _wantsExtendedDynamicRangeContent; |
| |
| - (instancetype)init { |
| if (self = [super init]) { |
| _preferredDevice = MTLCreateSystemDefaultDevice(); |
| self.device = self.preferredDevice; |
| self.pixelFormat = MTLPixelFormatBGRA8Unorm; |
| _availableTextures = [[NSMutableSet alloc] init]; |
| |
| _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)]; |
| [self setMaxRefreshRate:[DisplayLinkManager displayRefreshRate] forceMax:NO]; |
| [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; |
| [[NSNotificationCenter defaultCenter] addObserver:self |
| selector:@selector(didEnterBackground:) |
| name:UIApplicationDidEnterBackgroundNotification |
| object:nil]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| } |
| |
| - (void)setMaxRefreshRate:(double)refreshRate forceMax:(BOOL)forceMax { |
| // This is copied from vsync_waiter_ios.mm. The vsync waiter has display link scheduled on UI |
| // thread which does not trigger actual core animation frame. As a workaround FlutterMetalLayer |
| // has it's own displaylink scheduled on main thread, which is used to trigger core animation |
| // frame allowing for 120hz updates. |
| if (!DisplayLinkManager.maxRefreshRateEnabledOnIPhone) { |
| return; |
| } |
| double maxFrameRate = fmax(refreshRate, 60); |
| double minFrameRate = fmax(maxFrameRate / 2, 60); |
| if (@available(iOS 15.0, *)) { |
| _displayLink.preferredFrameRateRange = |
| CAFrameRateRangeMake(forceMax ? maxFrameRate : minFrameRate, maxFrameRate, maxFrameRate); |
| } else { |
| _displayLink.preferredFramesPerSecond = maxFrameRate; |
| } |
| } |
| |
| - (void)onDisplayLink:(CADisplayLink*)link { |
| _didSetContentsDuringThisDisplayLinkPeriod = NO; |
| // Do not pause immediately, this seems to prevent 120hz while touching. |
| if (_displayLinkPauseCountdown == 3) { |
| _displayLink.paused = YES; |
| if (_displayLinkForcedMaxRate) { |
| [self setMaxRefreshRate:[DisplayLinkManager displayRefreshRate] forceMax:NO]; |
| _displayLinkForcedMaxRate = NO; |
| } |
| } else { |
| ++_displayLinkPauseCountdown; |
| } |
| } |
| |
| - (BOOL)isKindOfClass:(Class)aClass { |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wunguarded-availability-new" |
| // Pretend that we're a CAMetalLayer so that the rest of Flutter plays along |
| if ([aClass isEqual:[CAMetalLayer class]]) { |
| return YES; |
| } |
| #pragma clang diagnostic pop |
| return [super isKindOfClass:aClass]; |
| } |
| |
| - (void)setDrawableSize:(CGSize)drawableSize { |
| [_availableTextures removeAllObjects]; |
| _front = nil; |
| _totalTextures = 0; |
| _drawableSize = drawableSize; |
| } |
| |
| - (void)didEnterBackground:(id)notification { |
| [_availableTextures removeAllObjects]; |
| _totalTextures = _front != nil ? 1 : 0; |
| _displayLink.paused = YES; |
| } |
| |
| - (CGSize)drawableSize { |
| return _drawableSize; |
| } |
| |
| - (IOSurface*)createIOSurface { |
| unsigned pixelFormat; |
| unsigned bytesPerElement; |
| if (self.pixelFormat == MTLPixelFormatRGBA16Float) { |
| pixelFormat = kCVPixelFormatType_64RGBAHalf; |
| bytesPerElement = 8; |
| } else if (self.pixelFormat == MTLPixelFormatBGRA8Unorm) { |
| pixelFormat = kCVPixelFormatType_32BGRA; |
| bytesPerElement = 4; |
| } else { |
| FML_LOG(ERROR) << "Unsupported pixel format: " << self.pixelFormat; |
| return nil; |
| } |
| size_t bytesPerRow = |
| IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, _drawableSize.width * bytesPerElement); |
| size_t totalBytes = |
| IOSurfaceAlignProperty(kIOSurfaceAllocSize, _drawableSize.height * bytesPerRow); |
| NSDictionary* options = @{ |
| (id)kIOSurfaceWidth : @(_drawableSize.width), |
| (id)kIOSurfaceHeight : @(_drawableSize.height), |
| (id)kIOSurfacePixelFormat : @(pixelFormat), |
| (id)kIOSurfaceBytesPerElement : @(bytesPerElement), |
| (id)kIOSurfaceBytesPerRow : @(bytesPerRow), |
| (id)kIOSurfaceAllocSize : @(totalBytes), |
| }; |
| |
| IOSurfaceRef res = IOSurfaceCreate((CFDictionaryRef)options); |
| if (res == nil) { |
| FML_LOG(ERROR) << "Failed to create IOSurface with options " |
| << options.debugDescription.UTF8String; |
| return nil; |
| } |
| |
| if (self.colorspace != nil) { |
| CFStringRef name = CGColorSpaceGetName(self.colorspace); |
| IOSurfaceSetValue(res, CFSTR("IOSurfaceColorSpace"), name); |
| } else { |
| IOSurfaceSetValue(res, CFSTR("IOSurfaceColorSpace"), kCGColorSpaceSRGB); |
| } |
| return (__bridge_transfer IOSurface*)res; |
| } |
| |
| - (FlutterTexture*)nextTexture { |
| CFTimeInterval start = CACurrentMediaTime(); |
| while (true) { |
| FlutterTexture* texture = [self tryNextTexture]; |
| if (texture != nil) { |
| return texture; |
| } |
| CFTimeInterval elapsed = CACurrentMediaTime() - start; |
| if (elapsed > 1.0) { |
| NSLog(@"Waited %f seconds for a drawable, giving up.", elapsed); |
| return nil; |
| } |
| } |
| } |
| |
| - (FlutterTexture*)tryNextTexture { |
| @synchronized(self) { |
| if (_front != nil && _front.waitingForCompletion) { |
| return nil; |
| } |
| if (_totalTextures < 3) { |
| ++_totalTextures; |
| IOSurface* surface = [self createIOSurface]; |
| if (surface == nil) { |
| return nil; |
| } |
| MTLTextureDescriptor* textureDescriptor = |
| [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:_pixelFormat |
| width:_drawableSize.width |
| height:_drawableSize.height |
| mipmapped:NO]; |
| |
| if (_framebufferOnly) { |
| textureDescriptor.usage = MTLTextureUsageRenderTarget; |
| } else { |
| textureDescriptor.usage = |
| MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; |
| } |
| id<MTLTexture> texture = [self.device newTextureWithDescriptor:textureDescriptor |
| iosurface:(__bridge IOSurfaceRef)surface |
| plane:0]; |
| FlutterTexture* flutterTexture = [[FlutterTexture alloc] initWithTexture:texture |
| surface:surface]; |
| return flutterTexture; |
| } else { |
| // Prefer surface that is not in use and has been presented the longest |
| // time ago. |
| // When isInUse is false, the surface is definitely not used by the compositor. |
| // When isInUse is true, the surface may be used by the compositor. |
| // When both surfaces are in use, the one presented earlier will be returned. |
| // The assumption here is that the compositor is already aware of the |
| // newer texture and is unlikely to read from the older one, even though it |
| // has not decreased the use count yet (there seems to be certain latency). |
| FlutterTexture* res = nil; |
| for (FlutterTexture* texture in _availableTextures) { |
| if (res == nil) { |
| res = texture; |
| } else if (res.surface.isInUse && !texture.surface.isInUse) { |
| // prefer texture that is not in use. |
| res = texture; |
| } else if (res.surface.isInUse == texture.surface.isInUse && |
| texture.presentedTime < res.presentedTime) { |
| // prefer texture with older presented time. |
| res = texture; |
| } |
| } |
| if (res != nil) { |
| [_availableTextures removeObject:res]; |
| } |
| return res; |
| } |
| } |
| } |
| |
| - (id<CAMetalDrawable>)nextDrawable { |
| FlutterTexture* texture = [self nextTexture]; |
| if (texture == nil) { |
| return nil; |
| } |
| FlutterDrawable* drawable = [[FlutterDrawable alloc] initWithTexture:texture |
| layer:self |
| drawableId:_nextDrawableId++]; |
| return drawable; |
| } |
| |
| - (void)presentOnMainThread:(FlutterTexture*)texture { |
| // This is needed otherwise frame gets skipped on touch begin / end. Go figure. |
| // Might also be placebo |
| [self setNeedsDisplay]; |
| |
| [CATransaction begin]; |
| [CATransaction setDisableActions:YES]; |
| self.contents = texture.surface; |
| [CATransaction commit]; |
| _displayLink.paused = NO; |
| _displayLinkPauseCountdown = 0; |
| if (!_didSetContentsDuringThisDisplayLinkPeriod) { |
| _didSetContentsDuringThisDisplayLinkPeriod = YES; |
| } else if (!_displayLinkForcedMaxRate) { |
| _displayLinkForcedMaxRate = YES; |
| [self setMaxRefreshRate:[DisplayLinkManager displayRefreshRate] forceMax:YES]; |
| } |
| } |
| |
| - (void)presentTexture:(FlutterTexture*)texture { |
| @synchronized(self) { |
| if (_front != nil) { |
| [_availableTextures addObject:_front]; |
| } |
| _front = texture; |
| texture.presentedTime = CACurrentMediaTime(); |
| if ([NSThread isMainThread]) { |
| [self presentOnMainThread:texture]; |
| } else { |
| // Core animation layers can only be updated on main thread. |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self presentOnMainThread:texture]; |
| }); |
| } |
| } |
| } |
| |
| - (void)returnTexture:(FlutterTexture*)texture { |
| @synchronized(self) { |
| [_availableTextures addObject:texture]; |
| } |
| } |
| |
| + (BOOL)enabled { |
| static BOOL enabled = NO; |
| static BOOL didCheckInfoPlist = NO; |
| if (!didCheckInfoPlist) { |
| didCheckInfoPlist = YES; |
| NSNumber* use_flutter_metal_layer = |
| [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTUseFlutterMetalLayer"]; |
| if (use_flutter_metal_layer != nil && [use_flutter_metal_layer boolValue]) { |
| enabled = YES; |
| FML_LOG(WARNING) << "Using FlutterMetalLayer. This is an experimental feature."; |
| } |
| } |
| return enabled; |
| } |
| |
| @end |