Flutter Engine
 
Loading...
Searching...
No Matches
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
6
7#import <Metal/Metal.h>
8
9#include <algorithm>
10
11#include "flutter/fml/logging.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
114 _backBufferCache = [[FlutterBackBufferCache alloc] init];
115 _frontSurfaces = [NSMutableArray array];
116 _layers = [NSMutableArray array];
117 }
118 return self;
119}
120
121- (FlutterBackBufferCache*)backBufferCache {
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
141- (BOOL)enableSurfaceDebugInfo {
142 if (_enableSurfaceDebugInfo == nil) {
143 _enableSurfaceDebugInfo =
144 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTEnableSurfaceDebugInfo"];
145 if (_enableSurfaceDebugInfo == nil) {
146 _enableSurfaceDebugInfo = @NO;
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
195 if (enableSurfaceDebugInfo) {
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 CGSize size = GetRequiredFrameSize(surfaces);
226
227 CFTimeInterval delay = 0;
228
229 if (presentationTime > 0) {
230 // Enforce frame pacing. It seems that the target timestamp of CVDisplayLink does not
231 // exactly correspond to core animation deadline. Especially with 120hz, setting the frame
232 // contents too close after previous target timestamp will result in uneven frame pacing.
233 // Empirically setting the content in the second half of frame interval seems to work
234 // well for both 60hz and 120hz.
235 //
236 // This schedules a timer on current (raster) thread runloop. Raster thread at
237 // this point should be idle (the next frame vsync has not been signalled yet).
238 //
239 // Alternative could be simply blocking the raster thread, but that would show
240 // as a average_frame_rasterizer_time_millis regresson.
241 CFTimeInterval minPresentationTime = (presentationTime + _lastPresentationTime) / 2.0;
242 CFTimeInterval now = CACurrentMediaTime();
243 delay = std::max(minPresentationTime - now, 0.0);
244 }
245 [_delegate onPresent:size
246 withBlock:^{
247 _lastPresentationTime = presentationTime;
248 [CATransaction begin];
249 [CATransaction setDisableActions:YES];
250 [self commit:surfaces];
251 if (notify != nil) {
252 notify();
253 }
254 [CATransaction commit];
255 }
256 delay:delay];
257}
258
259@end
260
261// Cached back buffers will be released after kIdleDelay if there is no activity.
262static const double kIdleDelay = 1.0;
263// Once surfaces reach kEvictionAge, they will be evicted from the cache.
264// The age of 30 has been chosen to reduce potential surface allocation churn.
265// For unused surface 30 frames means only half a second at 60fps, and there is
266// idle timeout of 1 second where all surfaces are evicted.
267static const int kSurfaceEvictionAge = 30;
268
270 NSMutableArray<FlutterSurface*>* _surfaces;
271 NSMapTable<FlutterSurface*, NSNumber*>* _surfaceAge;
272}
273
274@end
275
276@implementation FlutterBackBufferCache
277
278- (instancetype)init {
279 if (self = [super init]) {
280 self->_surfaces = [[NSMutableArray alloc] init];
281 self->_surfaceAge = [NSMapTable weakToStrongObjectsMapTable];
282 }
283 return self;
284}
285
286- (int)ageForSurface:(FlutterSurface*)surface {
287 NSNumber* age = [_surfaceAge objectForKey:surface];
288 return age != nil ? age.intValue : 0;
289}
290
291- (void)setAge:(int)age forSurface:(FlutterSurface*)surface {
292 [_surfaceAge setObject:@(age) forKey:surface];
293}
294
295- (nullable FlutterSurface*)removeSurfaceForSize:(CGSize)size {
296 @synchronized(self) {
297 // Purge all cached surfaces if the size has changed.
298 if (_surfaces.firstObject != nil && !CGSizeEqualToSize(_surfaces.firstObject.size, size)) {
299 [_surfaces removeAllObjects];
300 }
301
302 FlutterSurface* res;
303
304 // Returns youngest surface that is not in use. Returning youngest surface ensures
305 // that the cache doesn't keep more surfaces than it needs to, as the unused surfaces
306 // kept in cache will have their age kept increasing until purged (inside [returnSurfaces:]).
307 for (FlutterSurface* surface in _surfaces) {
308 if (!surface.isInUse &&
309 (res == nil || [self ageForSurface:res] > [self ageForSurface:surface])) {
310 res = surface;
311 }
312 }
313 if (res != nil) {
314 [_surfaces removeObject:res];
315 }
316 return res;
317 }
318}
319
320- (void)returnSurfaces:(nonnull NSArray<FlutterSurface*>*)returnedSurfaces {
321 @synchronized(self) {
322 for (FlutterSurface* surface in returnedSurfaces) {
323 [self setAge:0 forSurface:surface];
324 }
325 for (FlutterSurface* surface in _surfaces) {
326 [self setAge:[self ageForSurface:surface] + 1 forSurface:surface];
327 }
328
329 [_surfaces addObjectsFromArray:returnedSurfaces];
330
331 // Purge all surface with age = kSurfaceEvictionAge. Reaching this age can mean two things:
332 // - Surface is still in use and we can't return it. This can happen in some edge
333 // cases where the compositor holds on to the surface for much longer than expected.
334 // - Surface is not in use but it hasn't been requested from the cache for a while.
335 // This means there are too many surfaces in the cache.
336 [_surfaces filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FlutterSurface* surface,
337 NSDictionary* bindings) {
338 return [self ageForSurface:surface] < kSurfaceEvictionAge;
339 }]];
340 }
341
342 // performSelector:withObject:afterDelay needs to be performed on RunLoop thread
343 [self performSelectorOnMainThread:@selector(reschedule) withObject:nil waitUntilDone:NO];
344}
345
346- (NSUInteger)count {
347 @synchronized(self) {
348 return _surfaces.count;
349 }
350}
351
352- (void)onIdle {
353 @synchronized(self) {
354 [_surfaces removeAllObjects];
355 }
356}
357
358- (void)reschedule {
359 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil];
360 [self performSelector:@selector(onIdle) withObject:nil afterDelay:kIdleDelay];
361}
362
363- (void)dealloc {
364 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil];
365}
366
367@end
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
NSMutableArray< FlutterSurface * > * _surfaces
NSMapTable< FlutterSurface *, NSNumber * > * _surfaceAge
id< MTLCommandQueue > _commandQueue
NSMutableArray< FlutterSurface * > * _frontSurfaces
__weak id< FlutterSurfaceManagerDelegate > _delegate
FlutterBackBufferCache * _backBufferCache
NSMutableArray< CALayer * > * _layers
VkDevice device
Definition main.cc:69
VkSurfaceKHR surface
Definition main.cc:65
const FlutterLayer ** layers
#define FML_DCHECK(condition)
Definition logging.h:122
IOSurfaceRef ioSurface
std::vector< FlutterRect > paintRegion
it will be possible to load the file into Perfetto s trace viewer use test Running tests that layout and measure text will not yield consistent results across various platforms Enabling this option will make font resolution default to the Ahem test font on all 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
int32_t height
int32_t width
const uintptr_t id
int BOOL