Flutter iOS Embedder
FlutterMetalLayer.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #include <CoreMedia/CoreMedia.h>
8 #include <IOSurface/IOSurfaceObjC.h>
9 #include <Metal/Metal.h>
10 #include <UIKit/UIKit.h>
11 
12 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
14 #import "flutter/shell/platform/darwin/ios/InternalFlutterSwift/InternalFlutterSwift.h"
15 
17 
18 @class FlutterTexture;
19 @class FlutterDrawable;
20 
21 extern CFTimeInterval display_link_target;
22 
23 @interface FlutterMetalLayer () {
24  id<MTLDevice> _preferredDevice;
25  CGSize _drawableSize;
26 
27  NSUInteger _nextDrawableId;
28 
29  // Access to these variables must be synchronized.
30  NSMutableSet<FlutterTexture*>* _availableTextures;
31  NSUInteger _totalTextures;
33 
34  // There must be a CADisplayLink scheduled *on main thread* otherwise
35  // core animation only updates layers 60 times a second.
36  CADisplayLink* _displayLink;
38 
39  // Used to track whether the content was set during this display link.
40  // When unlocking phone the layer (main thread) display link and raster thread
41  // display link get out of sync for several seconds. Even worse, layer display
42  // link does not seem to reflect actual vsync. Forcing the layer link
43  // to max rate (instead range) temporarily seems to fix the issue.
45 
46  // Whether layer displayLink is forced to max rate.
48 }
49 
50 - (void)onDisplayLink:(CADisplayLink*)link;
51 - (void)presentTexture:(FlutterTexture*)texture;
52 - (void)returnTexture:(FlutterTexture*)texture;
53 
54 @end
55 
56 @interface FlutterTexture : NSObject
57 
58 @property(readonly, nonatomic) id<MTLTexture> texture;
59 @property(readonly, nonatomic) IOSurface* surface;
60 @property(readwrite, nonatomic) CFTimeInterval presentedTime;
61 @property(readwrite, atomic) BOOL waitingForCompletion;
62 
63 @end
64 
65 @implementation FlutterTexture
66 
67 - (instancetype)initWithTexture:(id<MTLTexture>)texture surface:(IOSurface*)surface {
68  if (self = [super init]) {
69  _texture = texture;
70  _surface = surface;
71  }
72  return self;
73 }
74 
75 @end
76 
77 @interface FlutterDrawable : NSObject <FlutterMetalDrawable> {
80  NSUInteger _drawableId;
81  BOOL _presented;
82 }
83 
84 - (instancetype)initWithTexture:(FlutterTexture*)texture
85  layer:(FlutterMetalLayer*)layer
86  drawableId:(NSUInteger)drawableId;
87 
88 @end
89 
90 @implementation FlutterDrawable
91 
92 - (instancetype)initWithTexture:(FlutterTexture*)texture
93  layer:(FlutterMetalLayer*)layer
94  drawableId:(NSUInteger)drawableId {
95  if (self = [super init]) {
96  _texture = texture;
97  _layer = layer;
98  _drawableId = drawableId;
99  }
100  return self;
101 }
102 
103 - (id<MTLTexture>)texture {
104  return self->_texture.texture;
105 }
106 
107 #pragma clang diagnostic push
108 #pragma clang diagnostic ignored "-Wunguarded-availability-new"
109 - (CAMetalLayer*)layer {
110  return (id)self->_layer;
111 }
112 #pragma clang diagnostic pop
113 
114 - (NSUInteger)drawableID {
115  return self->_drawableId;
116 }
117 
118 - (CFTimeInterval)presentedTime {
119  return 0;
120 }
121 
122 - (void)present {
123  [_layer presentTexture:self->_texture];
124  self->_presented = YES;
125 }
126 
127 - (void)dealloc {
128  if (!_presented) {
129  [_layer returnTexture:self->_texture];
130  }
131 }
132 
133 - (void)addPresentedHandler:(nonnull MTLDrawablePresentedHandler)block {
134  [FlutterLogger logWarning:@"FlutterMetalLayer drawable does not implement addPresentedHandler:"];
135 }
136 
137 - (void)presentAtTime:(CFTimeInterval)presentationTime {
138  [FlutterLogger logWarning:@"FlutterMetalLayer drawable does not implement presentAtTime:"];
139 }
140 
141 - (void)presentAfterMinimumDuration:(CFTimeInterval)duration {
142  [FlutterLogger
143  logWarning:@"FlutterMetalLayer drawable does not implement presentAfterMinimumDuration:"];
144 }
145 
146 - (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer {
148  texture.waitingForCompletion = YES;
149  [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
150  texture.waitingForCompletion = NO;
151  }];
152 }
153 
154 @end
155 
156 @interface FlutterMetalLayerDisplayLinkProxy : NSObject {
158 }
159 
160 @end
161 
162 @implementation FlutterMetalLayerDisplayLinkProxy
163 - (instancetype)initWithLayer:(FlutterMetalLayer*)layer {
164  if (self = [super init]) {
165  _layer = layer;
166  }
167  return self;
168 }
169 
170 - (void)onDisplayLink:(CADisplayLink*)link {
171  [_layer onDisplayLink:link];
172 }
173 
174 @end
175 
176 @implementation FlutterMetalLayer
177 
178 - (instancetype)init {
179  if (self = [super init]) {
180  _preferredDevice = MTLCreateSystemDefaultDevice();
181  self.device = self.preferredDevice;
182  self.pixelFormat = MTLPixelFormatBGRA8Unorm;
183  _availableTextures = [[NSMutableSet alloc] init];
184 
186  [[FlutterMetalLayerDisplayLinkProxy alloc] initWithLayer:self];
187  _displayLink = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(onDisplayLink:)];
188  [self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:NO];
189  [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
190  [[NSNotificationCenter defaultCenter] addObserver:self
191  selector:@selector(didEnterBackground:)
192  name:UIApplicationDidEnterBackgroundNotification
193  object:nil];
194  }
195  return self;
196 }
197 
198 - (void)dealloc {
199  [_displayLink invalidate];
200  [[NSNotificationCenter defaultCenter] removeObserver:self];
201 }
202 
203 - (void)setMaxRefreshRate:(double)refreshRate forceMax:(BOOL)forceMax {
204  // This is copied from vsync_waiter_ios.mm. The vsync waiter has display link scheduled on UI
205  // thread which does not trigger actual core animation frame. As a workaround FlutterMetalLayer
206  // has it's own displaylink scheduled on main thread, which is used to trigger core animation
207  // frame allowing for 120hz updates.
208  if (!FlutterDisplayLinkManager.maxRefreshRateEnabledOnIPhone) {
209  return;
210  }
211  double maxFrameRate = fmax(refreshRate, 60);
212  double minFrameRate = fmax(maxFrameRate / 2, 60);
213  _displayLink.preferredFrameRateRange =
214  CAFrameRateRangeMake(forceMax ? maxFrameRate : minFrameRate, maxFrameRate, maxFrameRate);
215 }
216 
217 - (void)onDisplayLink:(CADisplayLink*)link {
218  _didSetContentsDuringThisDisplayLinkPeriod = NO;
219  // Do not pause immediately, this seems to prevent 120hz while touching.
220  if (_displayLinkPauseCountdown == 3) {
221  _displayLink.paused = YES;
222  if (_displayLinkForcedMaxRate) {
223  [self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:NO];
224  _displayLinkForcedMaxRate = NO;
225  }
226  } else {
227  ++_displayLinkPauseCountdown;
228  }
229 }
230 
231 - (BOOL)isKindOfClass:(Class)aClass {
232 #pragma clang diagnostic push
233 #pragma clang diagnostic ignored "-Wunguarded-availability-new"
234  // Pretend that we're a CAMetalLayer so that the rest of Flutter plays along
235  if ([aClass isEqual:[CAMetalLayer class]]) {
236  return YES;
237  }
238 #pragma clang diagnostic pop
239  return [super isKindOfClass:aClass];
240 }
241 
242 - (void)setDrawableSize:(CGSize)drawableSize {
243  @synchronized(self) {
244  [_availableTextures removeAllObjects];
245  _front = nil;
246  _totalTextures = 0;
247  _drawableSize = drawableSize;
248  }
249 }
250 
251 - (void)didEnterBackground:(id)notification {
252  @synchronized(self) {
253  [_availableTextures removeAllObjects];
254  _totalTextures = _front != nil ? 1 : 0;
255  }
256  _displayLink.paused = YES;
257 }
258 
259 - (CGSize)drawableSize {
260  @synchronized(self) {
261  return _drawableSize;
262  }
263 }
264 
265 - (IOSurface*)createIOSurface {
266  unsigned pixelFormat;
267  unsigned bytesPerElement;
268  if (self.pixelFormat == MTLPixelFormatRGBA16Float) {
269  pixelFormat = kCVPixelFormatType_64RGBAHalf;
270  bytesPerElement = 8;
271  } else if (self.pixelFormat == MTLPixelFormatBGRA8Unorm) {
272  pixelFormat = kCVPixelFormatType_32BGRA;
273  bytesPerElement = 4;
274  } else if (self.pixelFormat == MTLPixelFormatBGRA10_XR) {
275  pixelFormat = kCVPixelFormatType_40ARGBLEWideGamut;
276  bytesPerElement = 8;
277  } else {
278  NSString* errorMessage =
279  [NSString stringWithFormat:@"Unsupported pixel format: %lu", self.pixelFormat];
280  [FlutterLogger logError:errorMessage];
281  return nil;
282  }
283  size_t bytesPerRow =
284  IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, _drawableSize.width * bytesPerElement);
285  size_t totalBytes =
286  IOSurfaceAlignProperty(kIOSurfaceAllocSize, _drawableSize.height * bytesPerRow);
287  NSDictionary* options = @{
288  (id)kIOSurfaceWidth : @(_drawableSize.width),
289  (id)kIOSurfaceHeight : @(_drawableSize.height),
290  (id)kIOSurfacePixelFormat : @(pixelFormat),
291  (id)kIOSurfaceBytesPerElement : @(bytesPerElement),
292  (id)kIOSurfaceBytesPerRow : @(bytesPerRow),
293  (id)kIOSurfaceAllocSize : @(totalBytes),
294  };
295 
296  IOSurfaceRef res = IOSurfaceCreate((CFDictionaryRef)options);
297  if (res == nil) {
298  NSString* errorMessage = [NSString
299  stringWithFormat:@"Failed to create IOSurface with options %@", options.debugDescription];
300  [FlutterLogger logError:errorMessage];
301  return nil;
302  }
303 
304  if (self.colorspace != nil) {
305  CFStringRef name = CGColorSpaceGetName(self.colorspace);
306  IOSurfaceSetValue(res, kIOSurfaceColorSpace, name);
307  } else {
308  IOSurfaceSetValue(res, kIOSurfaceColorSpace, kCGColorSpaceSRGB);
309  }
310  return (__bridge_transfer IOSurface*)res;
311 }
312 
313 - (FlutterTexture*)nextTexture {
314  CFTimeInterval start = CACurrentMediaTime();
315  while (true) {
316  FlutterTexture* texture = [self tryNextTexture];
317  if (texture != nil) {
318  return texture;
319  }
320  CFTimeInterval elapsed = CACurrentMediaTime() - start;
321  if (elapsed > 1.0) {
322  NSLog(@"Waited %f seconds for a drawable, giving up.", elapsed);
323  return nil;
324  }
325  }
326 }
327 
328 - (FlutterTexture*)tryNextTexture {
329  @synchronized(self) {
330  if (_front != nil && _front.waitingForCompletion) {
331  return nil;
332  }
333  if (_totalTextures < 3) {
334  ++_totalTextures;
335  IOSurface* surface = [self createIOSurface];
336  if (surface == nil) {
337  return nil;
338  }
339  MTLTextureDescriptor* textureDescriptor =
340  [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:_pixelFormat
341  width:_drawableSize.width
342  height:_drawableSize.height
343  mipmapped:NO];
344 
345  if (_framebufferOnly) {
346  textureDescriptor.usage = MTLTextureUsageRenderTarget;
347  } else {
348  textureDescriptor.usage =
349  MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite;
350  }
351  id<MTLTexture> texture = [self.device newTextureWithDescriptor:textureDescriptor
352  iosurface:(__bridge IOSurfaceRef)surface
353  plane:0];
354  FlutterTexture* flutterTexture = [[FlutterTexture alloc] initWithTexture:texture
355  surface:surface];
356  return flutterTexture;
357  } else {
358  // Prefer surface that is not in use and has been presented the longest
359  // time ago.
360  // When isInUse is false, the surface is definitely not used by the compositor.
361  // When isInUse is true, the surface may be used by the compositor.
362  // When both surfaces are in use, the one presented earlier will be returned.
363  // The assumption here is that the compositor is already aware of the
364  // newer texture and is unlikely to read from the older one, even though it
365  // has not decreased the use count yet (there seems to be certain latency).
366  FlutterTexture* res = nil;
367  for (FlutterTexture* texture in _availableTextures) {
368  if (res == nil) {
369  res = texture;
370  } else if (res.surface.isInUse && !texture.surface.isInUse) {
371  // prefer texture that is not in use.
372  res = texture;
373  } else if (res.surface.isInUse == texture.surface.isInUse &&
374  texture.presentedTime < res.presentedTime) {
375  // prefer texture with older presented time.
376  res = texture;
377  }
378  }
379  if (res != nil) {
380  [_availableTextures removeObject:res];
381  }
382  return res;
383  }
384  }
385 }
386 
387 - (id<CAMetalDrawable>)nextDrawable {
388  FlutterTexture* texture = [self nextTexture];
389  if (texture == nil) {
390  return nil;
391  }
392  FlutterDrawable* drawable = [[FlutterDrawable alloc] initWithTexture:texture
393  layer:self
394  drawableId:_nextDrawableId++];
395  return drawable;
396 }
397 
398 - (void)presentOnMainThread:(FlutterTexture*)texture {
399  if (texture.texture.width != _drawableSize.width ||
400  texture.texture.height != _drawableSize.height) {
401  // This texture was created with an old size, but the view has since been
402  // resized. Do not present this stale frame to avoid distortion. The texture
403  // will be correctly recycled on the next frame.
404  return;
405  }
406 
407  // This is needed otherwise frame gets skipped on touch begin / end. Go figure.
408  // Might also be placebo
409  [self setNeedsDisplay];
410 
411  [CATransaction begin];
412  [CATransaction setDisableActions:YES];
413  self.contents = texture.surface;
414  [CATransaction commit];
415  _displayLink.paused = NO;
416  _displayLinkPauseCountdown = 0;
417  if (!_didSetContentsDuringThisDisplayLinkPeriod) {
418  _didSetContentsDuringThisDisplayLinkPeriod = YES;
419  } else if (!_displayLinkForcedMaxRate) {
420  _displayLinkForcedMaxRate = YES;
421  [self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:YES];
422  }
423 }
424 
425 - (void)presentTexture:(FlutterTexture*)texture {
426  @synchronized(self) {
427  if (texture.texture.width != _drawableSize.width ||
428  texture.texture.height != _drawableSize.height) {
429  return;
430  }
431  if (_front != nil) {
432  [_availableTextures addObject:_front];
433  }
434  _front = texture;
435  texture.presentedTime = CACurrentMediaTime();
436  if ([NSThread isMainThread]) {
437  [self presentOnMainThread:texture];
438  } else {
439  // Core animation layers can only be updated on main thread.
440  dispatch_async(dispatch_get_main_queue(), ^{
441  [self presentOnMainThread:texture];
442  });
443  }
444  }
445 }
446 
447 - (void)returnTexture:(FlutterTexture*)texture {
448  if (texture == nil) {
449  return;
450  }
451  @synchronized(self) {
452  if (texture.texture.width == _drawableSize.width &&
453  texture.texture.height == _drawableSize.height) {
454  [_availableTextures addObject:texture];
455  }
456  }
457 }
458 
459 + (BOOL)enabled {
460  static BOOL enabled = YES;
461  static BOOL didCheckInfoPlist = NO;
462  if (!didCheckInfoPlist) {
463  didCheckInfoPlist = YES;
464  NSNumber* use_flutter_metal_layer =
465  [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTUseFlutterMetalLayer"];
466  if (use_flutter_metal_layer != nil && ![use_flutter_metal_layer boolValue]) {
467  enabled = NO;
468  }
469  }
470  return enabled;
471 }
472 
473 @end
CFTimeInterval display_link_target
id< MTLDevice > _preferredDevice
NSMutableSet< FlutterTexture * > * _availableTextures
BOOL _didSetContentsDuringThisDisplayLinkPeriod
CADisplayLink * _displayLink
FlutterTexture * _front
NSUInteger _displayLinkPauseCountdown
FlutterTexture * _texture
NSUInteger _drawableId
__weak FlutterMetalLayer * _layer
CGColorSpaceRef colorspace
nullable id< CAMetalDrawable > nextDrawable()
MTLPixelFormat pixelFormat
IOSurface * surface
id< MTLTexture > texture
CFTimeInterval presentedTime