| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h" |
| |
| #import <Metal/Metal.h> |
| |
| #include <algorithm> |
| |
| #include "flutter/fml/logging.h" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h" |
| |
| @implementation FlutterSurfacePresentInfo |
| @end |
| |
| @interface FlutterSurfaceManager () { |
| id<MTLDevice> _device; |
| id<MTLCommandQueue> _commandQueue; |
| CALayer* _containingLayer; |
| __weak id<FlutterSurfaceManagerDelegate> _delegate; |
| |
| // Available (cached) back buffer surfaces. These will be cleared during |
| // present and replaced by current frong surfaces. |
| FlutterBackBufferCache* _backBufferCache; |
| |
| // Surfaces currently used to back visible layers. |
| NSMutableArray<FlutterSurface*>* _frontSurfaces; |
| |
| // Currently visible layers. |
| NSMutableArray<CALayer*>* _layers; |
| |
| // Whether to highlight borders of overlay surfaces. Determined by |
| // FLTEnableSurfaceDebugInfo value in main bundle Info.plist. |
| NSNumber* _enableSurfaceDebugInfo; |
| CATextLayer* _infoLayer; |
| |
| CFTimeInterval _lastPresentationTime; |
| } |
| |
| /** |
| * Updates underlying CALayers with the contents of the surfaces to present. |
| */ |
| - (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces; |
| |
| @end |
| |
| static NSColor* GetBorderColorForLayer(int layer) { |
| NSArray* colors = @[ |
| [NSColor yellowColor], |
| [NSColor cyanColor], |
| [NSColor magentaColor], |
| [NSColor greenColor], |
| [NSColor purpleColor], |
| [NSColor orangeColor], |
| [NSColor blueColor], |
| ]; |
| return colors[layer % colors.count]; |
| } |
| |
| /// Creates sublayers for given layer, each one displaying a portion of the |
| /// of the surface determined by a rectangle in the provided paint region. |
| static void UpdateContentSubLayers(CALayer* layer, |
| IOSurfaceRef surface, |
| CGFloat scale, |
| CGSize surfaceSize, |
| NSColor* borderColor, |
| const std::vector<FlutterRect>& paintRegion) { |
| // Adjust sublayer count to paintRegion count. |
| while (layer.sublayers.count > paintRegion.size()) { |
| [layer.sublayers.lastObject removeFromSuperlayer]; |
| } |
| |
| while (layer.sublayers.count < paintRegion.size()) { |
| CALayer* newLayer = [CALayer layer]; |
| [layer addSublayer:newLayer]; |
| } |
| |
| for (size_t i = 0; i < paintRegion.size(); i++) { |
| CALayer* subLayer = [layer.sublayers objectAtIndex:i]; |
| const auto& rect = paintRegion[i]; |
| subLayer.frame = CGRectMake(rect.left / scale, rect.top / scale, |
| (rect.right - rect.left) / scale, (rect.bottom - rect.top) / scale); |
| |
| double width = surfaceSize.width; |
| double height = surfaceSize.height; |
| |
| subLayer.contentsRect = |
| CGRectMake(rect.left / width, rect.top / height, (rect.right - rect.left) / width, |
| (rect.bottom - rect.top) / height); |
| |
| if (borderColor != nil) { |
| // Visualize sublayer |
| subLayer.borderColor = borderColor.CGColor; |
| subLayer.borderWidth = 1.0; |
| } |
| |
| subLayer.contents = (__bridge id)surface; |
| } |
| } |
| |
| @implementation FlutterSurfaceManager |
| |
| - (instancetype)initWithDevice:(id<MTLDevice>)device |
| commandQueue:(id<MTLCommandQueue>)commandQueue |
| layer:(CALayer*)containingLayer |
| delegate:(__weak id<FlutterSurfaceManagerDelegate>)delegate { |
| if (self = [super init]) { |
| _device = device; |
| _commandQueue = commandQueue; |
| _containingLayer = containingLayer; |
| _delegate = delegate; |
| |
| _backBufferCache = [[FlutterBackBufferCache alloc] init]; |
| _frontSurfaces = [NSMutableArray array]; |
| _layers = [NSMutableArray array]; |
| } |
| return self; |
| } |
| |
| - (FlutterBackBufferCache*)backBufferCache { |
| return _backBufferCache; |
| } |
| |
| - (NSArray*)frontSurfaces { |
| return _frontSurfaces; |
| } |
| |
| - (NSArray*)layers { |
| return _layers; |
| } |
| |
| - (FlutterSurface*)surfaceForSize:(CGSize)size { |
| FlutterSurface* surface = [_backBufferCache removeSurfaceForSize:size]; |
| if (surface == nil) { |
| surface = [[FlutterSurface alloc] initWithSize:size device:_device]; |
| } |
| return surface; |
| } |
| |
| - (BOOL)enableSurfaceDebugInfo { |
| if (_enableSurfaceDebugInfo == nil) { |
| _enableSurfaceDebugInfo = |
| [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTEnableSurfaceDebugInfo"]; |
| if (_enableSurfaceDebugInfo == nil) { |
| _enableSurfaceDebugInfo = @NO; |
| } |
| } |
| return [_enableSurfaceDebugInfo boolValue]; |
| } |
| |
| - (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces { |
| FML_DCHECK([NSThread isMainThread]); |
| |
| // Release all unused back buffer surfaces and replace them with front surfaces. |
| [_backBufferCache replaceSurfaces:_frontSurfaces]; |
| |
| // Front surfaces will be replaced by currently presented surfaces. |
| [_frontSurfaces removeAllObjects]; |
| for (FlutterSurfacePresentInfo* info in surfaces) { |
| [_frontSurfaces addObject:info.surface]; |
| } |
| |
| // Add or remove layers to match the count of surfaces to present. |
| while (_layers.count > _frontSurfaces.count) { |
| [_layers.lastObject removeFromSuperlayer]; |
| [_layers removeLastObject]; |
| } |
| while (_layers.count < _frontSurfaces.count) { |
| CALayer* layer = [CALayer layer]; |
| [_containingLayer addSublayer:layer]; |
| [_layers addObject:layer]; |
| } |
| |
| bool enableSurfaceDebugInfo = self.enableSurfaceDebugInfo; |
| |
| // Update contents of surfaces. |
| for (size_t i = 0; i < surfaces.count; ++i) { |
| FlutterSurfacePresentInfo* info = surfaces[i]; |
| CALayer* layer = _layers[i]; |
| CGFloat scale = _containingLayer.contentsScale; |
| if (i == 0) { |
| layer.frame = CGRectMake(info.offset.x / scale, info.offset.y / scale, |
| info.surface.size.width / scale, info.surface.size.height / scale); |
| layer.contents = (__bridge id)info.surface.ioSurface; |
| } else { |
| layer.frame = CGRectZero; |
| NSColor* borderColor = enableSurfaceDebugInfo ? GetBorderColorForLayer(i - 1) : nil; |
| UpdateContentSubLayers(layer, info.surface.ioSurface, scale, info.surface.size, borderColor, |
| info.paintRegion); |
| } |
| layer.zPosition = info.zIndex; |
| } |
| |
| if (enableSurfaceDebugInfo) { |
| if (_infoLayer == nil) { |
| _infoLayer = [[CATextLayer alloc] init]; |
| [_containingLayer addSublayer:_infoLayer]; |
| _infoLayer.fontSize = 15; |
| _infoLayer.foregroundColor = [NSColor yellowColor].CGColor; |
| _infoLayer.frame = CGRectMake(15, 15, 300, 100); |
| _infoLayer.contentsScale = _containingLayer.contentsScale; |
| _infoLayer.zPosition = 100000; |
| } |
| _infoLayer.string = [NSString stringWithFormat:@"Surface count: %li", _layers.count]; |
| } |
| } |
| |
| static CGSize GetRequiredFrameSize(NSArray<FlutterSurfacePresentInfo*>* surfaces) { |
| CGSize size = CGSizeZero; |
| for (FlutterSurfacePresentInfo* info in surfaces) { |
| size = CGSizeMake(std::max(size.width, info.offset.x + info.surface.size.width), |
| std::max(size.height, info.offset.y + info.surface.size.height)); |
| } |
| return size; |
| } |
| |
| - (void)presentSurfaces:(NSArray<FlutterSurfacePresentInfo*>*)surfaces |
| atTime:(CFTimeInterval)presentationTime |
| notify:(dispatch_block_t)notify { |
| id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer]; |
| [commandBuffer commit]; |
| [commandBuffer waitUntilScheduled]; |
| |
| dispatch_block_t presentBlock = ^{ |
| // Get the actual dimensions of the frame (relevant for thread synchronizer). |
| CGSize size = GetRequiredFrameSize(surfaces); |
| [_delegate onPresent:size |
| withBlock:^{ |
| _lastPresentationTime = presentationTime; |
| [self commit:surfaces]; |
| if (notify != nil) { |
| notify(); |
| } |
| }]; |
| }; |
| |
| if (presentationTime > 0) { |
| // Enforce frame pacing. It seems that the target timestamp of CVDisplayLink does not |
| // exactly correspond to core animation deadline. Especially with 120hz, setting the frame |
| // contents too close after previous target timestamp will result in uneven frame pacing. |
| // Empirically setting the content in the second half of frame interval seems to work |
| // well for both 60hz and 120hz. |
| // |
| // This schedules a timer on current (raster) thread runloop. Raster thread at |
| // this point should be idle (the next frame vsync has not been signalled yet). |
| // |
| // Alternative could be simply blocking the raster thread, but that would show |
| // as a average_frame_rasterizer_time_millis regresson. |
| CFTimeInterval minPresentationTime = (presentationTime + _lastPresentationTime) / 2.0; |
| CFTimeInterval now = CACurrentMediaTime(); |
| if (now < minPresentationTime) { |
| NSTimer* timer = [NSTimer timerWithTimeInterval:minPresentationTime - now |
| repeats:NO |
| block:^(NSTimer* timer) { |
| presentBlock(); |
| }]; |
| [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; |
| return; |
| } |
| } |
| presentBlock(); |
| } |
| |
| @end |
| |
| // Cached back buffers will be released after kIdleDelay if there is no activity. |
| static const double kIdleDelay = 1.0; |
| |
| @interface FlutterBackBufferCache () { |
| NSMutableArray<FlutterSurface*>* _surfaces; |
| } |
| |
| @end |
| |
| @implementation FlutterBackBufferCache |
| |
| - (instancetype)init { |
| if (self = [super init]) { |
| self->_surfaces = [[NSMutableArray alloc] init]; |
| } |
| return self; |
| } |
| |
| - (nullable FlutterSurface*)removeSurfaceForSize:(CGSize)size { |
| @synchronized(self) { |
| for (FlutterSurface* surface in _surfaces) { |
| if (CGSizeEqualToSize(surface.size, size)) { |
| // By default ARC doesn't retain enumeration iteration variables. |
| FlutterSurface* res = surface; |
| [_surfaces removeObject:surface]; |
| return res; |
| } |
| } |
| return nil; |
| } |
| } |
| |
| - (void)replaceSurfaces:(nonnull NSArray<FlutterSurface*>*)surfaces { |
| @synchronized(self) { |
| [_surfaces removeAllObjects]; |
| [_surfaces addObjectsFromArray:surfaces]; |
| } |
| |
| // performSelector:withObject:afterDelay needs to be performed on RunLoop thread |
| [self performSelectorOnMainThread:@selector(reschedule) withObject:nil waitUntilDone:NO]; |
| } |
| |
| - (NSUInteger)count { |
| @synchronized(self) { |
| return _surfaces.count; |
| } |
| } |
| |
| - (void)onIdle { |
| @synchronized(self) { |
| [_surfaces removeAllObjects]; |
| } |
| } |
| |
| - (void)reschedule { |
| [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil]; |
| [self performSelector:@selector(onIdle) withObject:nil afterDelay:kIdleDelay]; |
| } |
| |
| - (void)dealloc { |
| [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onIdle) object:nil]; |
| } |
| |
| @end |