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