blob: 04471eb914863f786b0b3c027ab6982ec2d7c4c4 [file] [log] [blame]
// 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 <Cocoa/Cocoa.h>
#import <Metal/Metal.h>
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurfaceManager.h"
#include "flutter/testing/testing.h"
#include "gtest/gtest.h"
@interface TestView : NSView <FlutterSurfaceManagerDelegate>
@property(readwrite, nonatomic) CGSize presentedFrameSize;
- (nonnull instancetype)init;
@end
@implementation TestView
- (instancetype)init {
self = [super initWithFrame:NSZeroRect];
if (self) {
[self setWantsLayer:YES];
self.layer.contentsScale = 2.0;
}
return self;
}
- (void)onPresent:(CGSize)frameSize withBlock:(nonnull dispatch_block_t)block {
self.presentedFrameSize = frameSize;
block();
}
@end
namespace flutter::testing {
static FlutterSurfaceManager* CreateSurfaceManager(TestView* testView) {
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
id<MTLCommandQueue> commandQueue = [device newCommandQueue];
CALayer* layer = reinterpret_cast<CALayer*>(testView.layer);
return [[FlutterSurfaceManager alloc] initWithDevice:device
commandQueue:commandQueue
layer:layer
delegate:testView];
}
static FlutterSurfacePresentInfo* CreatePresentInfo(
FlutterSurface* surface,
CGPoint offset = CGPointZero,
size_t index = 0,
const std::vector<FlutterRect>& paintRegion = {}) {
FlutterSurfacePresentInfo* res = [[FlutterSurfacePresentInfo alloc] init];
res.surface = surface;
res.offset = offset;
res.zIndex = index;
res.paintRegion = paintRegion;
return res;
}
TEST(FlutterSurfaceManager, MetalTextureSizeMatchesSurfaceSize) {
TestView* testView = [[TestView alloc] init];
FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
// Get back buffer, lookup should work for borrowed surfaces util present.
auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)];
auto texture = surface.asFlutterMetalTexture;
id<MTLTexture> metalTexture = (__bridge id)texture.texture;
EXPECT_EQ(metalTexture.width, 100ul);
EXPECT_EQ(metalTexture.height, 50ul);
texture.destruction_callback(texture.user_data);
}
TEST(FlutterSurfaceManager, TestSurfaceLookupFromTexture) {
TestView* testView = [[TestView alloc] init];
FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
// Get back buffer, lookup should work for borrowed surfaces util present.
auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)];
// SurfaceManager should keep texture alive while borrowed.
auto texture = surface.asFlutterMetalTexture;
texture.destruction_callback(texture.user_data);
FlutterMetalTexture dummyTexture{.texture_id = 1, .texture = nullptr, .user_data = nullptr};
auto surface1 = [FlutterSurface fromFlutterMetalTexture:&dummyTexture];
EXPECT_EQ(surface1, nil);
auto surface2 = [FlutterSurface fromFlutterMetalTexture:&texture];
EXPECT_EQ(surface2, surface);
}
TEST(FlutterSurfaceManager, BackBufferCacheDoesNotLeak) {
TestView* testView = [[TestView alloc] init];
FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(110, 110)];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(120, 120)];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface3) ] atTime:0 notify:nil];
// Cache should be cleaned during present and only contain the last visible
// surface(s).
EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
auto surfaceFromCache = [surfaceManager surfaceForSize:CGSizeMake(110, 110)];
EXPECT_EQ(surfaceFromCache, surface2);
[surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
[surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
}
TEST(FlutterSurfaceManager, SurfacesAreRecycled) {
TestView* testView = [[TestView alloc] init];
FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
EXPECT_EQ(surfaceManager.frontSurfaces.count, 0ul);
// Get first surface and present it.
auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
EXPECT_TRUE(CGSizeEqualToSize(surface1.size, CGSizeMake(100, 100)));
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
EXPECT_EQ(surfaceManager.frontSurfaces.count, 0ul);
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil];
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
EXPECT_EQ(surfaceManager.frontSurfaces.count, 1ul);
EXPECT_EQ(testView.layer.sublayers.count, 1ul);
// Get second surface and present it.
auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
EXPECT_TRUE(CGSizeEqualToSize(surface2.size, CGSizeMake(100, 100)));
EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil];
// Check that current front surface returns to cache.
EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
EXPECT_EQ(surfaceManager.frontSurfaces.count, 1ul);
EXPECT_EQ(testView.layer.sublayers.count, 1ull);
// Check that surface is properly reused.
auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
EXPECT_EQ(surface3, surface1);
}
inline bool operator==(const CGRect& lhs, const CGRect& rhs) {
return CGRectEqualToRect(lhs, rhs);
}
TEST(FlutterSurfaceManager, LayerManagement) {
TestView* testView = [[TestView alloc] init];
FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
EXPECT_EQ(testView.layer.sublayers.count, 0ul);
auto surface1_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1_1, CGPointMake(20, 10)) ]
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 1ul);
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 40)));
auto surface2_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
auto surface2_2 = [surfaceManager surfaceForSize:CGSizeMake(20, 20)];
[surfaceManager presentSurfaces:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
FlutterRect{0, 0, 20, 20},
FlutterRect{40, 0, 60, 20},
})
]
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 2ul);
EXPECT_EQ(testView.layer.sublayers[0].zPosition, 1.0);
EXPECT_EQ(testView.layer.sublayers[1].zPosition, 2.0);
CALayer* firstOverlaySublayer;
{
NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
EXPECT_EQ(sublayers.count, 2ul);
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 0, 10, 10)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[1].frame, CGRectMake(20, 0, 10, 10)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].contentsRect, CGRectMake(0, 0, 1, 1)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[1].contentsRect, CGRectMake(2, 0, 1, 1)));
EXPECT_EQ(sublayers[0].contents, sublayers[1].contents);
firstOverlaySublayer = sublayers[0];
}
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 70)));
// Check second overlay sublayer is removed while first is reused and updated
[surfaceManager presentSurfaces:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
FlutterRect{0, 10, 20, 20},
})
]
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 2ul);
{
NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
EXPECT_EQ(sublayers.count, 1ul);
EXPECT_EQ(sublayers[0], firstOverlaySublayer);
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 5, 10, 5)));
}
// Check that second overlay sublayer is added back while first is reused and updated
[surfaceManager presentSurfaces:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
FlutterRect{0, 0, 20, 20},
FlutterRect{40, 0, 60, 20},
})
]
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 2ul);
{
NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
EXPECT_EQ(sublayers.count, 2ul);
EXPECT_EQ(sublayers[0], firstOverlaySublayer);
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 0, 10, 10)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[1].frame, CGRectMake(20, 0, 10, 10)));
EXPECT_EQ(sublayers[0].contents, sublayers[1].contents);
}
auto surface3_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
[surfaceManager presentSurfaces:@[ CreatePresentInfo(surface3_1, CGPointMake(20, 10)) ]
atTime:0
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 1ul);
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 40)));
// Check removal of all surfaces.
[surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 0ul);
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(0, 0)));
}
} // namespace flutter::testing