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