Flutter Engine
The Flutter Engine
FlutterSurfaceManager.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#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h"
6
7#import <Metal/Metal.h>
8
9#include <algorithm>
10
11#include "flutter/fml/logging.h"
12#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h"
13
14@implementation FlutterSurfacePresentInfo
15@end
16
17@interface FlutterSurfaceManager () {
18 id<MTLDevice> _device;
19 id<MTLCommandQueue> _commandQueue;
21 __weak id<FlutterSurfaceManagerDelegate> _delegate;
22
23 // Available (cached) back buffer surfaces. These will be cleared during
24 // present and replaced by current frong surfaces.
26
27 // Surfaces currently used to back visible layers.
28 NSMutableArray<FlutterSurface*>* _frontSurfaces;
29
30 // Currently visible layers.
31 NSMutableArray<CALayer*>* _layers;
32
33 // Whether to highlight borders of overlay surfaces. Determined by
34 // FLTEnableSurfaceDebugInfo value in main bundle Info.plist.
36 CATextLayer* _infoLayer;
37
38 CFTimeInterval _lastPresentationTime;
39}
40
41/**
42 * Updates underlying CALayers with the contents of the surfaces to present.
43 */
44- (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces;
45
46@end
47
48static NSColor* GetBorderColorForLayer(int layer) {
49 NSArray* colors = @[
50 [NSColor yellowColor],
51 [NSColor cyanColor],
52 [NSColor magentaColor],
53 [NSColor greenColor],
54 [NSColor purpleColor],
55 [NSColor orangeColor],
56 [NSColor blueColor],
57 ];
58 return colors[layer % colors.count];
59}
60
61/// Creates sublayers for given layer, each one displaying a portion of the
62/// of the surface determined by a rectangle in the provided paint region.
63static void UpdateContentSubLayers(CALayer* layer,
64 IOSurfaceRef surface,
65 CGFloat scale,
66 CGSize surfaceSize,
67 NSColor* borderColor,
68 const std::vector<FlutterRect>& paintRegion) {
69 // Adjust sublayer count to paintRegion count.
70 while (layer.sublayers.count > paintRegion.size()) {
71 [layer.sublayers.lastObject removeFromSuperlayer];
72 }
73
74 while (layer.sublayers.count < paintRegion.size()) {
75 CALayer* newLayer = [CALayer layer];
76 [layer addSublayer:newLayer];
77 }
78
79 for (size_t i = 0; i < paintRegion.size(); i++) {
80 CALayer* subLayer = [layer.sublayers objectAtIndex:i];
81 const auto& rect = paintRegion[i];
82 subLayer.frame = CGRectMake(rect.left / scale, rect.top / scale,
83 (rect.right - rect.left) / scale, (rect.bottom - rect.top) / scale);
84
85 double width = surfaceSize.width;
86 double height = surfaceSize.height;
87
88 subLayer.contentsRect =
89 CGRectMake(rect.left / width, rect.top / height, (rect.right - rect.left) / width,
90 (rect.bottom - rect.top) / height);
91
92 if (borderColor != nil) {
93 // Visualize sublayer
94 subLayer.borderColor = borderColor.CGColor;
95 subLayer.borderWidth = 1.0;
96 }
97
98 subLayer.contents = (__bridge id)surface;
99 }
100}
101
102@implementation FlutterSurfaceManager
103
104- (instancetype)initWithDevice:(id<MTLDevice>)device
105 commandQueue:(id<MTLCommandQueue>)commandQueue
106 layer:(CALayer*)containingLayer
107 delegate:(__weak id<FlutterSurfaceManagerDelegate>)delegate {
108 if (self = [super init]) {
109 _device = device;
110 _commandQueue = commandQueue;
111 _containingLayer = containingLayer;
112 _delegate = delegate;
113
115 _frontSurfaces = [NSMutableArray array];
116 _layers = [NSMutableArray array];
117 }
118 return self;
119}
120
122 return _backBufferCache;
123}
124
125- (NSArray*)frontSurfaces {
126 return _frontSurfaces;
127}
128
129- (NSArray*)layers {
130 return _layers;
131}
132
133- (FlutterSurface*)surfaceForSize:(CGSize)size {
134 FlutterSurface* surface = [_backBufferCache removeSurfaceForSize:size];
135 if (surface == nil) {
136 surface = [[FlutterSurface alloc] initWithSize:size device:_device];
137 }
138 return surface;
139}
140
142 if (_enableSurfaceDebugInfo == nil) {
144 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTEnableSurfaceDebugInfo"];
145 if (_enableSurfaceDebugInfo == nil) {
147 }
148 }
149 return [_enableSurfaceDebugInfo boolValue];
150}
151
152- (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces {
153 FML_DCHECK([NSThread isMainThread]);
154
155 // Release all unused back buffer surfaces and replace them with front surfaces.
156 [_backBufferCache returnSurfaces:_frontSurfaces];
157
158 // Front surfaces will be replaced by currently presented surfaces.
159 [_frontSurfaces removeAllObjects];
160 for (FlutterSurfacePresentInfo* info in surfaces) {
161 [_frontSurfaces addObject:info.surface];
162 }
163
164 // Add or remove layers to match the count of surfaces to present.
165 while (_layers.count > _frontSurfaces.count) {
166 [_layers.lastObject removeFromSuperlayer];
167 [_layers removeLastObject];
168 }
169 while (_layers.count < _frontSurfaces.count) {
170 CALayer* layer = [CALayer layer];
171 [_containingLayer addSublayer:layer];
172 [_layers addObject:layer];
173 }
174
175 bool enableSurfaceDebugInfo = self.enableSurfaceDebugInfo;
176
177 // Update contents of surfaces.
178 for (size_t i = 0; i < surfaces.count; ++i) {
179 FlutterSurfacePresentInfo* info = surfaces[i];
180 CALayer* layer = _layers[i];
181 CGFloat scale = _containingLayer.contentsScale;
182 if (i == 0) {
183 layer.frame = CGRectMake(info.offset.x / scale, info.offset.y / scale,
184 info.surface.size.width / scale, info.surface.size.height / scale);
185 layer.contents = (__bridge id)info.surface.ioSurface;
186 } else {
187 layer.frame = CGRectZero;
188 NSColor* borderColor = enableSurfaceDebugInfo ? GetBorderColorForLayer(i - 1) : nil;
189 UpdateContentSubLayers(layer, info.surface.ioSurface, scale, info.surface.size, borderColor,
190 info.paintRegion);
191 }
192 layer.zPosition = info.zIndex;
193 }
194
196 if (_infoLayer == nil) {
197 _infoLayer = [[CATextLayer alloc] init];
198 [_containingLayer addSublayer:_infoLayer];
199 _infoLayer.fontSize = 15;
200 _infoLayer.foregroundColor = [NSColor yellowColor].CGColor;
201 _infoLayer.frame = CGRectMake(15, 15, 300, 100);
202 _infoLayer.contentsScale = _containingLayer.contentsScale;
203 _infoLayer.zPosition = 100000;
204 }
205 _infoLayer.string = [NSString stringWithFormat:@"Surface count: %li", _layers.count];
206 }
207}
208
209static CGSize GetRequiredFrameSize(NSArray<FlutterSurfacePresentInfo*>* surfaces) {
210 CGSize size = CGSizeZero;
211 for (FlutterSurfacePresentInfo* info in surfaces) {
212 size = CGSizeMake(std::max(size.width, info.offset.x + info.surface.size.width),
213 std::max(size.height, info.offset.y + info.surface.size.height));
214 }
215 return size;
216}
217
218- (void)presentSurfaces:(NSArray<FlutterSurfacePresentInfo*>*)surfaces
219 atTime:(CFTimeInterval)presentationTime
220 notify:(dispatch_block_t)notify {
221 id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
222 [commandBuffer commit];
223 [commandBuffer waitUntilScheduled];
224
225 dispatch_block_t presentBlock = ^{
226 // Get the actual dimensions of the frame (relevant for thread synchronizer).
227 CGSize size = GetRequiredFrameSize(surfaces);
228 [_delegate onPresent:size
229 withBlock:^{
230 _lastPresentationTime = presentationTime;
231 [self commit:surfaces];
232 if (notify != nil) {
233 notify();
234 }
235 }];
236 };
237
238 if (presentationTime > 0) {
239 // Enforce frame pacing. It seems that the target timestamp of CVDisplayLink does not
240 // exactly correspond to core animation deadline. Especially with 120hz, setting the frame
241 // contents too close after previous target timestamp will result in uneven frame pacing.
242 // Empirically setting the content in the second half of frame interval seems to work
243 // well for both 60hz and 120hz.
244 //
245 // This schedules a timer on current (raster) thread runloop. Raster thread at
246 // this point should be idle (the next frame vsync has not been signalled yet).
247 //
248 // Alternative could be simply blocking the raster thread, but that would show
249 // as a average_frame_rasterizer_time_millis regresson.
250 CFTimeInterval minPresentationTime = (presentationTime + _lastPresentationTime) / 2.0;
251 CFTimeInterval now = CACurrentMediaTime();
252 if (now < minPresentationTime) {
253 NSTimer* timer = [NSTimer timerWithTimeInterval:minPresentationTime - now
254 repeats:NO
255 block:^(NSTimer* timer) {
256 presentBlock();
257 }];
258 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
259 return;
260 }
261 }
262 presentBlock();
263}
264
265@end
266
267// Cached back buffers will be released after kIdleDelay if there is no activity.
268static const double kIdleDelay = 1.0;
269// Once surfaces reach kEvictionAge, they will be evicted from the cache.
270// The age of 30 has been chosen to reduce potential surface allocation churn.
271// For unused surface 30 frames means only half a second at 60fps, and there is
272// idle timeout of 1 second where all surfaces are evicted.
273static const int kSurfaceEvictionAge = 30;
274
275@interface FlutterBackBufferCache () {
276 NSMutableArray<FlutterSurface*>* _surfaces;
277 NSMapTable<FlutterSurface*, NSNumber*>* _surfaceAge;
278}
279
280@end
281
282@implementation FlutterBackBufferCache
283
284- (instancetype)init {
285 if (self = [super init]) {
286 self->_surfaces = [[NSMutableArray alloc] init];
287 self->_surfaceAge = [NSMapTable weakToStrongObjectsMapTable];
288 }
289 return self;
290}
291
292- (int)ageForSurface:(FlutterSurface*)surface {
293 NSNumber* age = [_surfaceAge objectForKey:surface];
294 return age != nil ? age.intValue : 0;
295}
296
297- (void)setAge:(int)age forSurface:(FlutterSurface*)surface {
298 [_surfaceAge setObject:@(age) forKey:surface];
299}
300
301- (nullable FlutterSurface*)removeSurfaceForSize:(CGSize)size {
302 @synchronized(self) {
303 // Purge all cached surfaces if the size has changed.
304 if (_surfaces.firstObject != nil && !CGSizeEqualToSize(_surfaces.firstObject.size, size)) {
305 [_surfaces removeAllObjects];
306 }
307
308 FlutterSurface* res;
309
310 // Returns youngest surface that is not in use. Returning youngest surface ensures
311 // that the cache doesn't keep more surfaces than it needs to, as the unused surfaces
312 // kept in cache will have their age kept increasing until purged (inside [returnSurfaces:]).
314 if (!surface.isInUse &&
315 (res == nil || [self ageForSurface:res] > [self ageForSurface:surface])) {
316 res = surface;
317 }
318 }
319 if (res != nil) {
320 [_surfaces removeObject:res];
321 }
322 return res;
323 }
324}
325
326- (void)returnSurfaces:(nonnull NSArray<FlutterSurface*>*)returnedSurfaces {
327 @synchronized(self) {
328 for (FlutterSurface* surface in returnedSurfaces) {
329 [self setAge:0 forSurface:surface];
330 }
332 [self setAge:[self ageForSurface:surface] + 1 forSurface:surface];
333 }
334
335 [_surfaces addObjectsFromArray:returnedSurfaces];
336
337 // Purge all surface with age = kSurfaceEvictionAge. Reaching this age can mean two things:
338 // - Surface is still in use and we can't return it. This can happen in some edge
339 // cases where the compositor holds on to the surface for much longer than expected.
340 // - Surface is not in use but it hasn't been requested from the cache for a while.
341 // This means there are too many surfaces in the cache.
342 [_surfaces filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FlutterSurface* surface,
343 NSDictionary* bindings) {
344 return [self ageForSurface:surface] < kSurfaceEvictionAge;
345 }]];
346 }
347
348 // performSelector:withObject:afterDelay needs to be performed on RunLoop thread
349 [self performSelectorOnMainThread:@selector(reschedule) withObject:nil waitUntilDone:NO];
350}
351
352- (NSUInteger)count {
353 @synchronized(self) {
354 return _surfaces.count;
355 }
356}
357
358- (void)onIdle {
359 @synchronized(self) {
360 [_surfaces removeAllObjects];
361 }
362}
363
364- (void)reschedule {
365 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil];
366 [self performSelector:@selector(onIdle) withObject:nil afterDelay:kIdleDelay];
367}
368
369- (void)dealloc {
370 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil];
371}
372
373@end
static void info(const char *fmt,...) SK_PRINTF_LIKE(1
Definition: DM.cpp:213
static void UpdateContentSubLayers(CALayer *layer, IOSurfaceRef surface, CGFloat scale, CGSize surfaceSize, NSColor *borderColor, const std::vector< FlutterRect > &paintRegion)
static NSColor * GetBorderColorForLayer(int layer)
static const double kIdleDelay
static const int kSurfaceEvictionAge
int count
Definition: FontMgrTest.cpp:50
FlutterBackBufferCache * backBufferCache
NSArray< FlutterSurface * > * frontSurfaces
VkDevice device
Definition: main.cc:53
VkSurfaceKHR surface
Definition: main.cc:49
#define FML_DCHECK(condition)
Definition: logging.h:103
static float max(float r, float g, float b)
Definition: hsl.cpp:49
NSMutableArray< FlutterSurface * > * _surfaces
NSMapTable< FlutterSurface *, NSNumber * > * _surfaceAge
static CGSize GetRequiredFrameSize(NSArray< FlutterSurfacePresentInfo * > *surfaces)
id< MTLCommandQueue > _commandQueue
NSMutableArray< FlutterSurface * > * _frontSurfaces
__weak id< FlutterSurfaceManagerDelegate > _delegate
FlutterBackBufferCache * _backBufferCache
CFTimeInterval _lastPresentationTime
NSMutableArray< CALayer * > * _layers
static bool init()
sk_sp< SkBlender > blender SkRect rect
Definition: SkRecords.h:350
PODArray< SkColor > colors
Definition: SkRecords.h:276
it will be possible to load the file into Perfetto s trace viewer disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
Definition: switches.h:259
int32_t height
int32_t width
const Scalar scale
const uintptr_t id
int BOOL
Definition: windows_types.h:37