Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
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;
23
24 // Available (cached) back buffer surfaces. These will be cleared during
25 // present and replaced by current frong surfaces.
27
28 // Surfaces currently used to back visible layers.
29 NSMutableArray<FlutterSurface*>* _frontSurfaces;
30
31 // Currently visible layers.
32 NSMutableArray<CALayer*>* _layers;
33
34 // Whether to highlight borders of overlay surfaces. Determined by
35 // FLTEnableSurfaceDebugInfo value in main bundle Info.plist.
37 CATextLayer* _infoLayer;
38
39 CFTimeInterval _lastPresentationTime;
40}
41
42/**
43 * Updates underlying CALayers with the contents of the surfaces to present.
44 */
45- (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces;
46
47@end
48
49static NSColor* GetBorderColorForLayer(int layer) {
50 NSArray* colors = @[
51 [NSColor yellowColor],
52 [NSColor cyanColor],
53 [NSColor magentaColor],
54 [NSColor greenColor],
55 [NSColor purpleColor],
56 [NSColor orangeColor],
57 [NSColor blueColor],
58 ];
59 return colors[layer % colors.count];
60}
61
62/// Creates sublayers for given layer, each one displaying a portion of the
63/// of the surface determined by a rectangle in the provided paint region.
64static void UpdateContentSubLayers(CALayer* layer,
65 IOSurfaceRef surface,
66 CGFloat scale,
67 CGSize surfaceSize,
68 NSColor* borderColor,
69 const std::vector<FlutterRect>& paintRegion) {
70 // Adjust sublayer count to paintRegion count.
71 while (layer.sublayers.count > paintRegion.size()) {
72 [layer.sublayers.lastObject removeFromSuperlayer];
73 }
74
75 while (layer.sublayers.count < paintRegion.size()) {
76 CALayer* newLayer = [CALayer layer];
77 [layer addSublayer:newLayer];
78 }
79
80 for (size_t i = 0; i < paintRegion.size(); i++) {
81 CALayer* subLayer = [layer.sublayers objectAtIndex:i];
82 const auto& rect = paintRegion[i];
83 subLayer.frame = CGRectMake(rect.left / scale, rect.top / scale,
84 (rect.right - rect.left) / scale, (rect.bottom - rect.top) / scale);
85
86 double width = surfaceSize.width;
87 double height = surfaceSize.height;
88
89 subLayer.contentsRect =
90 CGRectMake(rect.left / width, rect.top / height, (rect.right - rect.left) / width,
91 (rect.bottom - rect.top) / height);
92
93 if (borderColor != nil) {
94 // Visualize sublayer
95 subLayer.borderColor = borderColor.CGColor;
96 subLayer.borderWidth = 1.0;
97 }
98
99 subLayer.contents = (__bridge id)surface;
100 }
101}
102
103@implementation FlutterSurfaceManager
104
105- (instancetype)initWithDevice:(id<MTLDevice>)device
106 commandQueue:(id<MTLCommandQueue>)commandQueue
107 layer:(CALayer*)containingLayer
108 delegate:(__weak id<FlutterSurfaceManagerDelegate>)delegate
109 wideGamut:(BOOL)wideGamut {
110 if (self = [super init]) {
111 _device = device;
112 _commandQueue = commandQueue;
113 _containingLayer = containingLayer;
114 _delegate = delegate;
115 _wideGamut = wideGamut;
116 _backBufferCache = [[FlutterBackBufferCache alloc] init];
117 _frontSurfaces = [NSMutableArray array];
118 _layers = [NSMutableArray array];
119 }
120 return self;
121}
122
123- (void)setEnableWideGamut:(BOOL)enableWideGamut {
124 FML_DCHECK([NSThread isMainThread]);
125 if (_wideGamut == enableWideGamut) {
126 return;
127 }
128 _wideGamut = enableWideGamut;
129
130 // Flush cached surfaces since they have the wrong pixel format.
131 [_backBufferCache flush];
132
133 // Clear front surfaces — they will be replaced on the next present.
134 [_frontSurfaces removeAllObjects];
135}
136
137- (FlutterBackBufferCache*)backBufferCache {
138 return _backBufferCache;
139}
140
141- (NSArray*)frontSurfaces {
142 return _frontSurfaces;
143}
144
145- (NSArray*)layers {
146 return _layers;
147}
148
149- (FlutterSurface*)surfaceForSize:(CGSize)size {
150 FlutterSurface* surface = [_backBufferCache removeSurfaceForSize:size];
151 if (surface == nil) {
152 surface = [[FlutterSurface alloc] initWithSize:size device:_device enableWideGamut:_wideGamut];
153 }
154 return surface;
155}
156
157- (BOOL)enableSurfaceDebugInfo {
158 if (_enableSurfaceDebugInfo == nil) {
159 _enableSurfaceDebugInfo =
160 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTEnableSurfaceDebugInfo"];
161 if (_enableSurfaceDebugInfo == nil) {
162 _enableSurfaceDebugInfo = @NO;
163 }
164 }
165 return [_enableSurfaceDebugInfo boolValue];
166}
167
168- (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces {
169 FML_DCHECK([NSThread isMainThread]);
170
171 // Check if incoming surfaces match current wide gamut mode.
172 // If not, discard them by returning early - they will be released.
173 for (FlutterSurfacePresentInfo* info in surfaces) {
174 if (info.surface.isWideGamut != _wideGamut) {
175 return;
176 }
177 }
178
179 // Release all unused back buffer surfaces and replace them with front surfaces.
180 [_backBufferCache returnSurfaces:_frontSurfaces];
181
182 // Front surfaces will be replaced by currently presented surfaces.
183 [_frontSurfaces removeAllObjects];
184 for (FlutterSurfacePresentInfo* info in surfaces) {
185 [_frontSurfaces addObject:info.surface];
186 }
187
188 // Add or remove layers to match the count of surfaces to present.
189 while (_layers.count > _frontSurfaces.count) {
190 [_layers.lastObject removeFromSuperlayer];
191 [_layers removeLastObject];
192 }
193 while (_layers.count < _frontSurfaces.count) {
194 CALayer* layer = [CALayer layer];
195 [_containingLayer addSublayer:layer];
196 [_layers addObject:layer];
197 }
198
199 bool enableSurfaceDebugInfo = self.enableSurfaceDebugInfo;
200
201 // Update contents of surfaces.
202 for (size_t i = 0; i < surfaces.count; ++i) {
203 FlutterSurfacePresentInfo* info = surfaces[i];
204 CALayer* layer = _layers[i];
205 CGFloat scale = _containingLayer.contentsScale;
206 if (i == 0) {
207 layer.frame = CGRectMake(info.offset.x / scale, info.offset.y / scale,
208 info.surface.size.width / scale, info.surface.size.height / scale);
209 layer.contents = (__bridge id)info.surface.ioSurface;
210 } else {
211 layer.frame = CGRectZero;
212 NSColor* borderColor = enableSurfaceDebugInfo ? GetBorderColorForLayer(i - 1) : nil;
213 UpdateContentSubLayers(layer, info.surface.ioSurface, scale, info.surface.size, borderColor,
214 info.paintRegion);
215 }
216 layer.zPosition = info.zIndex;
217 }
218
219 if (enableSurfaceDebugInfo) {
220 if (_infoLayer == nil) {
221 _infoLayer = [[CATextLayer alloc] init];
222 [_containingLayer addSublayer:_infoLayer];
223 _infoLayer.fontSize = 15;
224 _infoLayer.foregroundColor = [NSColor yellowColor].CGColor;
225 _infoLayer.frame = CGRectMake(15, 15, 300, 100);
226 _infoLayer.contentsScale = _containingLayer.contentsScale;
227 _infoLayer.zPosition = 100000;
228 }
229 _infoLayer.string = [NSString stringWithFormat:@"Surface count: %li", _layers.count];
230 }
231}
232
233static CGSize GetRequiredFrameSize(NSArray<FlutterSurfacePresentInfo*>* surfaces) {
234 CGSize size = CGSizeZero;
235 for (FlutterSurfacePresentInfo* info in surfaces) {
236 size = CGSizeMake(std::max(size.width, info.offset.x + info.surface.size.width),
237 std::max(size.height, info.offset.y + info.surface.size.height));
238 }
239 return size;
240}
241
242- (void)presentSurfaces:(NSArray<FlutterSurfacePresentInfo*>*)surfaces
243 atTime:(CFTimeInterval)presentationTime
244 notify:(dispatch_block_t)notify {
245 id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
246 [commandBuffer commit];
247 [commandBuffer waitUntilScheduled];
248
249 CGSize size = GetRequiredFrameSize(surfaces);
250
251 CFTimeInterval delay = 0;
252
253 if (presentationTime > 0) {
254 // Enforce frame pacing. It seems that the target timestamp of CVDisplayLink does not
255 // exactly correspond to core animation deadline. Especially with 120hz, setting the frame
256 // contents too close after previous target timestamp will result in uneven frame pacing.
257 // Empirically setting the content in the second half of frame interval seems to work
258 // well for both 60hz and 120hz.
259 //
260 // This schedules a timer on current (raster) thread runloop. Raster thread at
261 // this point should be idle (the next frame vsync has not been signalled yet).
262 //
263 // Alternative could be simply blocking the raster thread, but that would show
264 // as a average_frame_rasterizer_time_millis regresson.
265 CFTimeInterval minPresentationTime = (presentationTime + _lastPresentationTime) / 2.0;
266 CFTimeInterval now = CACurrentMediaTime();
267 delay = std::max(minPresentationTime - now, 0.0);
268 }
269 [_delegate onPresent:size
270 withBlock:^{
271 _lastPresentationTime = presentationTime;
272 [CATransaction begin];
273 [CATransaction setDisableActions:YES];
274 [self commit:surfaces];
275 if (notify != nil) {
276 notify();
277 }
278 [CATransaction commit];
279 }
280 delay:delay];
281}
282
283@end
284
285// Cached back buffers will be released after kIdleDelay if there is no activity.
286static const double kIdleDelay = 1.0;
287// Once surfaces reach kEvictionAge, they will be evicted from the cache.
288// The age of 30 has been chosen to reduce potential surface allocation churn.
289// For unused surface 30 frames means only half a second at 60fps, and there is
290// idle timeout of 1 second where all surfaces are evicted.
291static const int kSurfaceEvictionAge = 30;
292
294 NSMutableArray<FlutterSurface*>* _surfaces;
295 NSMapTable<FlutterSurface*, NSNumber*>* _surfaceAge;
296}
297
298@end
299
300@implementation FlutterBackBufferCache
301
302- (instancetype)init {
303 if (self = [super init]) {
304 self->_surfaces = [[NSMutableArray alloc] init];
305 self->_surfaceAge = [NSMapTable weakToStrongObjectsMapTable];
306 }
307 return self;
308}
309
310- (int)ageForSurface:(FlutterSurface*)surface {
311 NSNumber* age = [_surfaceAge objectForKey:surface];
312 return age != nil ? age.intValue : 0;
313}
314
315- (void)setAge:(int)age forSurface:(FlutterSurface*)surface {
316 [_surfaceAge setObject:@(age) forKey:surface];
317}
318
319- (nullable FlutterSurface*)removeSurfaceForSize:(CGSize)size {
320 @synchronized(self) {
321 // Purge all cached surfaces if the size has changed.
322 if (_surfaces.firstObject != nil && !CGSizeEqualToSize(_surfaces.firstObject.size, size)) {
323 [_surfaces removeAllObjects];
324 }
325
326 FlutterSurface* res;
327
328 // Returns youngest surface that is not in use. Returning youngest surface ensures
329 // that the cache doesn't keep more surfaces than it needs to, as the unused surfaces
330 // kept in cache will have their age kept increasing until purged (inside [returnSurfaces:]).
331 for (FlutterSurface* surface in _surfaces) {
332 if (!surface.isInUse &&
333 (res == nil || [self ageForSurface:res] > [self ageForSurface:surface])) {
334 res = surface;
335 }
336 }
337 if (res != nil) {
338 [_surfaces removeObject:res];
339 }
340 return res;
341 }
342}
343
344- (void)returnSurfaces:(nonnull NSArray<FlutterSurface*>*)returnedSurfaces {
345 @synchronized(self) {
346 for (FlutterSurface* surface in returnedSurfaces) {
347 [self setAge:0 forSurface:surface];
348 }
349 for (FlutterSurface* surface in _surfaces) {
350 [self setAge:[self ageForSurface:surface] + 1 forSurface:surface];
351 }
352
353 [_surfaces addObjectsFromArray:returnedSurfaces];
354
355 // Purge all surface with age = kSurfaceEvictionAge. Reaching this age can mean two things:
356 // - Surface is still in use and we can't return it. This can happen in some edge
357 // cases where the compositor holds on to the surface for much longer than expected.
358 // - Surface is not in use but it hasn't been requested from the cache for a while.
359 // This means there are too many surfaces in the cache.
360 [_surfaces filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FlutterSurface* surface,
361 NSDictionary* bindings) {
362 return [self ageForSurface:surface] < kSurfaceEvictionAge;
363 }]];
364 }
365
366 // performSelector:withObject:afterDelay needs to be performed on RunLoop thread
367 [self performSelectorOnMainThread:@selector(reschedule) withObject:nil waitUntilDone:NO];
368}
369
370- (void)flush {
371 @synchronized(self) {
372 [_surfaces removeAllObjects];
373 }
374}
375
376- (NSUInteger)count {
377 @synchronized(self) {
378 return _surfaces.count;
379 }
380}
381
382- (void)onIdle {
383 [self flush];
384}
385
386- (void)reschedule {
387 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil];
388 [self performSelector:@selector(onIdle) withObject:nil afterDelay:kIdleDelay];
389}
390
391- (void)dealloc {
392 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil];
393}
394
395@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