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 
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 
48 static 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.
63 static 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 
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 
209 static 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.
268 static 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.
273 static 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:]).
313  for (FlutterSurface* surface in _surfaces) {
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  }
331  for (FlutterSurface* surface in _surfaces) {
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
FlutterBackBufferCache()::_surfaces
NSMutableArray< FlutterSurface * > * _surfaces
Definition: FlutterSurfaceManager.mm:276
GetBorderColorForLayer
static NSColor * GetBorderColorForLayer(int layer)
Definition: FlutterSurfaceManager.mm:48
FlutterSurfaceManager(Private)::layers
NSArray< CALayer * > * layers
Definition: FlutterSurfaceManager.h:107
FlutterSurfaceManager()::_frontSurfaces
NSMutableArray< FlutterSurface * > * _frontSurfaces
Definition: FlutterSurfaceManager.mm:28
FlutterSurfaceManager()::_layers
NSMutableArray< CALayer * > * _layers
Definition: FlutterSurfaceManager.mm:31
kSurfaceEvictionAge
static const int kSurfaceEvictionAge
Definition: FlutterSurfaceManager.mm:273
FlutterSurface::ioSurface
IOSurfaceRef ioSurface
Definition: FlutterSurface.h:32
FlutterSurfaceManager(Private)::frontSurfaces
NSArray< FlutterSurface * > * frontSurfaces
Definition: FlutterSurfaceManager.h:106
FlutterSurfaceManager()::_containingLayer
CALayer * _containingLayer
Definition: FlutterSurfaceManager.mm:20
FlutterSurfaceManager.h
-[FlutterBackBufferCache count]
NSUInteger count()
Definition: FlutterSurfaceManager.mm:352
FlutterSurfaceManager
Definition: FlutterSurfaceManager.h:44
FlutterSurfaceManager()::_lastPresentationTime
CFTimeInterval _lastPresentationTime
Definition: FlutterSurfaceManager.mm:38
FlutterSurface::size
CGSize size
Definition: FlutterSurface.h:33
FlutterSurfaceManager()::_backBufferCache
FlutterBackBufferCache * _backBufferCache
Definition: FlutterSurfaceManager.mm:25
FlutterBackBufferCache
Definition: FlutterSurfaceManager.h:81
FlutterSurfaceManager()::_commandQueue
id< MTLCommandQueue > _commandQueue
Definition: FlutterSurfaceManager.mm:19
FlutterSurfaceManager()::_infoLayer
CATextLayer * _infoLayer
Definition: FlutterSurfaceManager.mm:36
FlutterSurface.h
FlutterSurface
Definition: FlutterSurface.h:16
FlutterSurfaceManagerDelegate-p
Definition: FlutterSurfaceManager.h:27
FlutterSurfaceManager()::_delegate
__weak id< FlutterSurfaceManagerDelegate > _delegate
Definition: FlutterSurfaceManager.mm:21
FlutterSurfacePresentInfo::surface
FlutterSurface * surface
Definition: FlutterSurfaceManager.h:20
kIdleDelay
static const double kIdleDelay
Definition: FlutterSurfaceManager.mm:268
FlutterSurfacePresentInfo::offset
CGPoint offset
Definition: FlutterSurfaceManager.h:21
UpdateContentSubLayers
static void UpdateContentSubLayers(CALayer *layer, IOSurfaceRef surface, CGFloat scale, CGSize surfaceSize, NSColor *borderColor, const std::vector< FlutterRect > &paintRegion)
Definition: FlutterSurfaceManager.mm:63
FlutterSurfacePresentInfo
Definition: FlutterSurfaceManager.h:18
FlutterSurfacePresentInfo::zIndex
size_t zIndex
Definition: FlutterSurfaceManager.h:22
FlutterSurface::isInUse
BOOL isInUse
Definition: FlutterSurface.h:36
FlutterSurfacePresentInfo::paintRegion
std::vector< FlutterRect > paintRegion
Definition: FlutterSurfaceManager.h:23
FlutterBackBufferCache()::_surfaceAge
NSMapTable< FlutterSurface *, NSNumber * > * _surfaceAge
Definition: FlutterSurfaceManager.mm:277
FlutterSurfaceManager()::_device
id< MTLDevice > _device
Definition: FlutterSurfaceManager.mm:18
FlutterSurfaceManager()::_enableSurfaceDebugInfo
NSNumber * _enableSurfaceDebugInfo
Definition: FlutterSurfaceManager.mm:35
FlutterSurfaceManager(Private)::backBufferCache
FlutterBackBufferCache * backBufferCache
Definition: FlutterSurfaceManager.h:105