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
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]) {
70 _surface = surface;
71 }
72 return self;
73}
74
75@end
76
77@interface FlutterDrawable : NSObject <FlutterMetalDrawable> {
80 NSUInteger _drawableId;
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]) {
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 {
149 [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
150 texture.waitingForCompletion = NO;
151 }];
152}
153
154@end
155
158}
159
160@end
161
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 if (@available(iOS 15.0, *)) {
214 _displayLink.preferredFrameRateRange =
215 CAFrameRateRangeMake(forceMax ? maxFrameRate : minFrameRate, maxFrameRate, maxFrameRate);
216 } else {
217 _displayLink.preferredFramesPerSecond = maxFrameRate;
218 }
219}
220
221- (void)onDisplayLink:(CADisplayLink*)link {
222 _didSetContentsDuringThisDisplayLinkPeriod = NO;
223 // Do not pause immediately, this seems to prevent 120hz while touching.
224 if (_displayLinkPauseCountdown == 3) {
225 _displayLink.paused = YES;
226 if (_displayLinkForcedMaxRate) {
227 [self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:NO];
228 _displayLinkForcedMaxRate = NO;
229 }
230 } else {
231 ++_displayLinkPauseCountdown;
232 }
233}
234
235- (BOOL)isKindOfClass:(Class)aClass {
236#pragma clang diagnostic push
237#pragma clang diagnostic ignored "-Wunguarded-availability-new"
238 // Pretend that we're a CAMetalLayer so that the rest of Flutter plays along
239 if ([aClass isEqual:[CAMetalLayer class]]) {
240 return YES;
241 }
242#pragma clang diagnostic pop
243 return [super isKindOfClass:aClass];
244}
245
246- (void)setDrawableSize:(CGSize)drawableSize {
247 @synchronized(self) {
248 [_availableTextures removeAllObjects];
249 _front = nil;
250 _totalTextures = 0;
251 _drawableSize = drawableSize;
252 }
253}
254
255- (void)didEnterBackground:(id)notification {
256 @synchronized(self) {
257 [_availableTextures removeAllObjects];
258 _totalTextures = _front != nil ? 1 : 0;
259 }
260 _displayLink.paused = YES;
261}
262
263- (CGSize)drawableSize {
264 @synchronized(self) {
265 return _drawableSize;
266 }
267}
268
269- (IOSurface*)createIOSurface {
270 unsigned pixelFormat;
271 unsigned bytesPerElement;
272 if (self.pixelFormat == MTLPixelFormatRGBA16Float) {
273 pixelFormat = kCVPixelFormatType_64RGBAHalf;
274 bytesPerElement = 8;
275 } else if (self.pixelFormat == MTLPixelFormatBGRA8Unorm) {
276 pixelFormat = kCVPixelFormatType_32BGRA;
277 bytesPerElement = 4;
278 } else if (self.pixelFormat == MTLPixelFormatBGRA10_XR) {
279 pixelFormat = kCVPixelFormatType_40ARGBLEWideGamut;
280 bytesPerElement = 8;
281 } else {
282 NSString* errorMessage =
283 [NSString stringWithFormat:@"Unsupported pixel format: %lu", self.pixelFormat];
284 [FlutterLogger logError:errorMessage];
285 return nil;
286 }
287 size_t bytesPerRow =
288 IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, _drawableSize.width * bytesPerElement);
289 size_t totalBytes =
290 IOSurfaceAlignProperty(kIOSurfaceAllocSize, _drawableSize.height * bytesPerRow);
291 NSDictionary* options = @{
292 (id)kIOSurfaceWidth : @(_drawableSize.width),
293 (id)kIOSurfaceHeight : @(_drawableSize.height),
294 (id)kIOSurfacePixelFormat : @(pixelFormat),
295 (id)kIOSurfaceBytesPerElement : @(bytesPerElement),
296 (id)kIOSurfaceBytesPerRow : @(bytesPerRow),
297 (id)kIOSurfaceAllocSize : @(totalBytes),
298 };
299
300 IOSurfaceRef res = IOSurfaceCreate((CFDictionaryRef)options);
301 if (res == nil) {
302 NSString* errorMessage = [NSString
303 stringWithFormat:@"Failed to create IOSurface with options %@", options.debugDescription];
304 [FlutterLogger logError:errorMessage];
305 return nil;
306 }
307
308 if (self.colorspace != nil) {
309 CFStringRef name = CGColorSpaceGetName(self.colorspace);
310 IOSurfaceSetValue(res, kIOSurfaceColorSpace, name);
311 } else {
312 IOSurfaceSetValue(res, kIOSurfaceColorSpace, kCGColorSpaceSRGB);
313 }
314 return (__bridge_transfer IOSurface*)res;
315}
316
317- (FlutterTexture*)nextTexture {
318 CFTimeInterval start = CACurrentMediaTime();
319 while (true) {
320 FlutterTexture* texture = [self tryNextTexture];
321 if (texture != nil) {
322 return texture;
323 }
324 CFTimeInterval elapsed = CACurrentMediaTime() - start;
325 if (elapsed > 1.0) {
326 NSLog(@"Waited %f seconds for a drawable, giving up.", elapsed);
327 return nil;
328 }
329 }
330}
331
332- (FlutterTexture*)tryNextTexture {
333 @synchronized(self) {
334 if (_front != nil && _front.waitingForCompletion) {
335 return nil;
336 }
337 if (_totalTextures < 3) {
338 ++_totalTextures;
339 IOSurface* surface = [self createIOSurface];
340 if (surface == nil) {
341 return nil;
342 }
343 MTLTextureDescriptor* textureDescriptor =
344 [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:_pixelFormat
345 width:_drawableSize.width
346 height:_drawableSize.height
347 mipmapped:NO];
348
349 if (_framebufferOnly) {
350 textureDescriptor.usage = MTLTextureUsageRenderTarget;
351 } else {
352 textureDescriptor.usage =
353 MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite;
354 }
355 id<MTLTexture> texture = [self.device newTextureWithDescriptor:textureDescriptor
356 iosurface:(__bridge IOSurfaceRef)surface
357 plane:0];
358 FlutterTexture* flutterTexture = [[FlutterTexture alloc] initWithTexture:texture
359 surface:surface];
360 return flutterTexture;
361 } else {
362 // Prefer surface that is not in use and has been presented the longest
363 // time ago.
364 // When isInUse is false, the surface is definitely not used by the compositor.
365 // When isInUse is true, the surface may be used by the compositor.
366 // When both surfaces are in use, the one presented earlier will be returned.
367 // The assumption here is that the compositor is already aware of the
368 // newer texture and is unlikely to read from the older one, even though it
369 // has not decreased the use count yet (there seems to be certain latency).
370 FlutterTexture* res = nil;
371 for (FlutterTexture* texture in _availableTextures) {
372 if (res == nil) {
373 res = texture;
374 } else if (res.surface.isInUse && !texture.surface.isInUse) {
375 // prefer texture that is not in use.
376 res = texture;
377 } else if (res.surface.isInUse == texture.surface.isInUse &&
378 texture.presentedTime < res.presentedTime) {
379 // prefer texture with older presented time.
380 res = texture;
381 }
382 }
383 if (res != nil) {
384 [_availableTextures removeObject:res];
385 }
386 return res;
387 }
388 }
389}
390
391- (id<CAMetalDrawable>)nextDrawable {
392 FlutterTexture* texture = [self nextTexture];
393 if (texture == nil) {
394 return nil;
395 }
396 FlutterDrawable* drawable = [[FlutterDrawable alloc] initWithTexture:texture
397 layer:self
398 drawableId:_nextDrawableId++];
399 return drawable;
400}
401
402- (void)presentOnMainThread:(FlutterTexture*)texture {
403 if (texture.texture.width != _drawableSize.width ||
404 texture.texture.height != _drawableSize.height) {
405 // This texture was created with an old size, but the view has since been
406 // resized. Do not present this stale frame to avoid distortion. The texture
407 // will be correctly recycled on the next frame.
408 return;
409 }
410
411 // This is needed otherwise frame gets skipped on touch begin / end. Go figure.
412 // Might also be placebo
413 [self setNeedsDisplay];
414
415 [CATransaction begin];
416 [CATransaction setDisableActions:YES];
417 self.contents = texture.surface;
418 [CATransaction commit];
419 _displayLink.paused = NO;
420 _displayLinkPauseCountdown = 0;
421 if (!_didSetContentsDuringThisDisplayLinkPeriod) {
422 _didSetContentsDuringThisDisplayLinkPeriod = YES;
423 } else if (!_displayLinkForcedMaxRate) {
424 _displayLinkForcedMaxRate = YES;
425 [self setMaxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate forceMax:YES];
426 }
427}
428
429- (void)presentTexture:(FlutterTexture*)texture {
430 @synchronized(self) {
431 if (texture.texture.width != _drawableSize.width ||
432 texture.texture.height != _drawableSize.height) {
433 return;
434 }
435 if (_front != nil) {
436 [_availableTextures addObject:_front];
437 }
438 _front = texture;
439 texture.presentedTime = CACurrentMediaTime();
440 if ([NSThread isMainThread]) {
441 [self presentOnMainThread:texture];
442 } else {
443 // Core animation layers can only be updated on main thread.
444 dispatch_async(dispatch_get_main_queue(), ^{
445 [self presentOnMainThread:texture];
446 });
447 }
448 }
449}
450
451- (void)returnTexture:(FlutterTexture*)texture {
452 if (texture == nil) {
453 return;
454 }
455 @synchronized(self) {
456 if (texture.texture.width == _drawableSize.width &&
457 texture.texture.height == _drawableSize.height) {
458 [_availableTextures addObject:texture];
459 }
460 }
461}
462
463+ (BOOL)enabled {
464 static BOOL enabled = YES;
465 static BOOL didCheckInfoPlist = NO;
466 if (!didCheckInfoPlist) {
467 didCheckInfoPlist = YES;
468 NSNumber* use_flutter_metal_layer =
469 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTUseFlutterMetalLayer"];
470 if (use_flutter_metal_layer != nil && ![use_flutter_metal_layer boolValue]) {
471 enabled = NO;
472 }
473 }
474 return enabled;
475}
476
477@end
id< FlutterTexture > _texture
CFTimeInterval display_link_target
FlutterDisplayLink * _displayLink
id< MTLDevice > _preferredDevice
NSMutableSet< FlutterTexture * > * _availableTextures
BOOL _didSetContentsDuringThisDisplayLinkPeriod
CADisplayLink * _displayLink
FlutterTexture * _front
NSUInteger _displayLinkPauseCountdown
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