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