Flutter macOS Embedder
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;
20  CALayer* _containingLayer;
21  __weak id<FlutterSurfaceManagerDelegate> _delegate;
22  BOOL _wideGamut;
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 
49 static 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.
64 static 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 
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 
233 static 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.
286 static 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.
291 static const int kSurfaceEvictionAge = 30;
292 
293 @interface FlutterBackBufferCache () {
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
FlutterBackBufferCache * backBufferCache
NSArray< FlutterSurface * > * frontSurfaces
IOSurfaceRef ioSurface
std::vector< FlutterRect > paintRegion