Flutter Engine
 
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
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
25extern CFTimeInterval display_link_target;
26
27@interface FlutterMetalLayer () {
28 id<MTLDevice> _preferredDevice;
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]) {
74 _surface = surface;
75 }
76 return self;
77}
78
79@end
80
81@interface FlutterDrawable : NSObject <FlutterMetalDrawable> {
84 NSUInteger _drawableId;
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]) {
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 {
153 [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
154 texture.waitingForCompletion = NO;
155 }];
156}
157
158@end
159
162}
163
164@end
165
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
id< FlutterTexture > _texture
CFTimeInterval display_link_target
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:49
FlutterTexture * _texture
__weak FlutterMetalLayer * _layer
CFTimeInterval presentedTime
FlTexture * texture
int32_t height
int32_t width
const size_t start
const uintptr_t id
CADisplayLink * _displayLink
int BOOL