Flutter macOS Embedder
FlutterSurfaceManagerTest.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 
5 #import <Cocoa/Cocoa.h>
6 #import <Metal/Metal.h>
7 
10 
11 #include "flutter/testing/testing.h"
12 #include "gtest/gtest.h"
13 
15 
16 @property(readwrite, nonatomic) CGSize presentedFrameSize;
17 - (nonnull instancetype)init;
18 
19 @end
20 
21 @implementation TestView
22 
23 - (instancetype)init {
24  self = [super initWithFrame:NSZeroRect];
25  if (self) {
26  [self setWantsLayer:YES];
27  self.layer.contentsScale = 2.0;
28  }
29  return self;
30 }
31 
32 - (void)onPresent:(CGSize)frameSize
33  withBlock:(nonnull dispatch_block_t)block
34  delay:(NSTimeInterval)delay {
35  self.presentedFrameSize = frameSize;
36  block();
37 }
38 
39 @end
40 
41 namespace flutter::testing {
42 
44  id<MTLDevice> device = MTLCreateSystemDefaultDevice();
45  id<MTLCommandQueue> commandQueue = [device newCommandQueue];
46  CALayer* layer = reinterpret_cast<CALayer*>(testView.layer);
47  return [[FlutterSurfaceManager alloc] initWithDevice:device
48  commandQueue:commandQueue
49  layer:layer
50  delegate:testView];
51 }
52 
54  FlutterSurface* surface,
55  CGPoint offset = CGPointZero,
56  size_t index = 0,
57  const std::vector<FlutterRect>& paintRegion = {}) {
59  res.surface = surface;
60  res.offset = offset;
61  res.zIndex = index;
62  res.paintRegion = paintRegion;
63  return res;
64 }
65 
66 TEST(FlutterSurfaceManager, MetalTextureSizeMatchesSurfaceSize) {
67  TestView* testView = [[TestView alloc] init];
68  FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
69 
70  // Get back buffer, lookup should work for borrowed surfaces util present.
71  auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)];
72  auto texture = surface.asFlutterMetalTexture;
73  id<MTLTexture> metalTexture = (__bridge id)texture.texture;
74  EXPECT_EQ(metalTexture.width, 100ul);
75  EXPECT_EQ(metalTexture.height, 50ul);
76  texture.destruction_callback(texture.user_data);
77 }
78 
79 TEST(FlutterSurfaceManager, TestSurfaceLookupFromTexture) {
80  TestView* testView = [[TestView alloc] init];
81  FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
82 
83  // Get back buffer, lookup should work for borrowed surfaces util present.
84  auto surface = [surfaceManager surfaceForSize:CGSizeMake(100, 50)];
85 
86  // SurfaceManager should keep texture alive while borrowed.
87  auto texture = surface.asFlutterMetalTexture;
88  texture.destruction_callback(texture.user_data);
89 
90  FlutterMetalTexture dummyTexture{.texture_id = 1, .texture = nullptr, .user_data = nullptr};
91  auto surface1 = [FlutterSurface fromFlutterMetalTexture:&dummyTexture];
92  EXPECT_EQ(surface1, nil);
93 
94  auto surface2 = [FlutterSurface fromFlutterMetalTexture:&texture];
95  EXPECT_EQ(surface2, surface);
96 }
97 
98 TEST(FlutterSurfaceManager, BackBufferCacheDoesNotLeak) {
99  TestView* testView = [[TestView alloc] init];
100  FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
101  EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
102 
103  auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
104  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil];
105 
106  EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
107 
108  auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(110, 110)];
109  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil];
110 
111  EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
112 
113  auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(120, 120)];
114  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface3) ] atTime:0 notify:nil];
115 
116  // Cache should be cleaned during present and only contain the last visible
117  // surface(s).
118  EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
119  auto surfaceFromCache = [surfaceManager surfaceForSize:CGSizeMake(110, 110)];
120  EXPECT_EQ(surfaceFromCache, surface2);
121 
122  // Submit empty surfaces until the one in cache gets to age >= kSurfaceEvictionAge, in which case
123  // it should be removed.
124 
125  for (int i = 0; i < 30 /* kSurfaceEvictionAge */; ++i) {
126  [surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
127  EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
128  }
129 
130  [surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
131  EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
132 }
133 
134 TEST(FlutterSurfaceManager, SurfacesAreRecycled) {
135  TestView* testView = [[TestView alloc] init];
136  FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
137 
138  EXPECT_EQ(surfaceManager.frontSurfaces.count, 0ul);
139 
140  // Get first surface and present it.
141 
142  auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
143  EXPECT_TRUE(CGSizeEqualToSize(surface1.size, CGSizeMake(100, 100)));
144 
145  EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
146  EXPECT_EQ(surfaceManager.frontSurfaces.count, 0ul);
147 
148  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil];
149 
150  EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
151  EXPECT_EQ(surfaceManager.frontSurfaces.count, 1ul);
152  EXPECT_EQ(testView.layer.sublayers.count, 1ul);
153 
154  // Get second surface and present it.
155 
156  auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
157  EXPECT_TRUE(CGSizeEqualToSize(surface2.size, CGSizeMake(100, 100)));
158 
159  EXPECT_EQ(surfaceManager.backBufferCache.count, 0ul);
160 
161  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil];
162 
163  // Check that current front surface returns to cache.
164  EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
165  EXPECT_EQ(surfaceManager.frontSurfaces.count, 1ul);
166  EXPECT_EQ(testView.layer.sublayers.count, 1ull);
167 
168  // Check that surface is properly reused.
169  auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
170  EXPECT_EQ(surface3, surface1);
171 }
172 
173 TEST(FlutterSurfaceManager, BackingStoreCacheSurfaceStuckInUse) {
174  TestView* testView = [[TestView alloc] init];
175  FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
176 
177  auto surface1 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
178 
179  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1) ] atTime:0 notify:nil];
180  // Pretend that compositor is holding on to the surface. The surface will be kept
181  // in cache until the age of kSurfaceEvictionAge is reached, and then evicted.
182  surface1.isInUseOverride = YES;
183 
184  auto surface2 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
185  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface2) ] atTime:0 notify:nil];
186  EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
187 
188  for (int i = 0; i < 30 /* kSurfaceEvictionAge */ - 1; ++i) {
189  auto surface3 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
190  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface3) ] atTime:0 notify:nil];
191  EXPECT_EQ(surfaceManager.backBufferCache.count, 2ul);
192  }
193 
194  auto surface4 = [surfaceManager surfaceForSize:CGSizeMake(100, 100)];
195  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface4) ] atTime:0 notify:nil];
196  // Surface in use should bet old enough at this point to be evicted.
197  EXPECT_EQ(surfaceManager.backBufferCache.count, 1ul);
198 }
199 
200 inline bool operator==(const CGRect& lhs, const CGRect& rhs) {
201  return CGRectEqualToRect(lhs, rhs);
202 }
203 
204 TEST(FlutterSurfaceManager, LayerManagement) {
205  TestView* testView = [[TestView alloc] init];
206  FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
207 
208  EXPECT_EQ(testView.layer.sublayers.count, 0ul);
209 
210  auto surface1_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
211  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface1_1, CGPointMake(20, 10)) ]
212  atTime:0
213  notify:nil];
214 
215  EXPECT_EQ(testView.layer.sublayers.count, 1ul);
216  EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 40)));
217 
218  auto surface2_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
219  auto surface2_2 = [surfaceManager surfaceForSize:CGSizeMake(20, 20)];
220  [surfaceManager presentSurfaces:@[
221  CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
222  CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
223  {
224  FlutterRect{0, 0, 20, 20},
225  FlutterRect{40, 0, 60, 20},
226  })
227  ]
228  atTime:0
229  notify:nil];
230 
231  EXPECT_EQ(testView.layer.sublayers.count, 2ul);
232  EXPECT_EQ(testView.layer.sublayers[0].zPosition, 1.0);
233  EXPECT_EQ(testView.layer.sublayers[1].zPosition, 2.0);
234  CALayer* firstOverlaySublayer;
235  {
236  NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
237  EXPECT_EQ(sublayers.count, 2ul);
238  EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 0, 10, 10)));
239  EXPECT_TRUE(CGRectEqualToRect(sublayers[1].frame, CGRectMake(20, 0, 10, 10)));
240  EXPECT_TRUE(CGRectEqualToRect(sublayers[0].contentsRect, CGRectMake(0, 0, 1, 1)));
241  EXPECT_TRUE(CGRectEqualToRect(sublayers[1].contentsRect, CGRectMake(2, 0, 1, 1)));
242  EXPECT_EQ(sublayers[0].contents, sublayers[1].contents);
243  firstOverlaySublayer = sublayers[0];
244  }
245  EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 70)));
246 
247  // Check second overlay sublayer is removed while first is reused and updated
248  [surfaceManager presentSurfaces:@[
249  CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
250  CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
251  {
252  FlutterRect{0, 10, 20, 20},
253  })
254  ]
255  atTime:0
256  notify:nil];
257  EXPECT_EQ(testView.layer.sublayers.count, 2ul);
258  {
259  NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
260  EXPECT_EQ(sublayers.count, 1ul);
261  EXPECT_EQ(sublayers[0], firstOverlaySublayer);
262  EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 5, 10, 5)));
263  }
264 
265  // Check that second overlay sublayer is added back while first is reused and updated
266  [surfaceManager presentSurfaces:@[
267  CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
268  CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
269  {
270  FlutterRect{0, 0, 20, 20},
271  FlutterRect{40, 0, 60, 20},
272  })
273  ]
274  atTime:0
275  notify:nil];
276 
277  EXPECT_EQ(testView.layer.sublayers.count, 2ul);
278  {
279  NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
280  EXPECT_EQ(sublayers.count, 2ul);
281  EXPECT_EQ(sublayers[0], firstOverlaySublayer);
282  EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 0, 10, 10)));
283  EXPECT_TRUE(CGRectEqualToRect(sublayers[1].frame, CGRectMake(20, 0, 10, 10)));
284  EXPECT_EQ(sublayers[0].contents, sublayers[1].contents);
285  }
286 
287  auto surface3_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
288  [surfaceManager presentSurfaces:@[ CreatePresentInfo(surface3_1, CGPointMake(20, 10)) ]
289  atTime:0
290  notify:nil];
291 
292  EXPECT_EQ(testView.layer.sublayers.count, 1ul);
293  EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 40)));
294 
295  // Check removal of all surfaces.
296  [surfaceManager presentSurfaces:@[] atTime:0 notify:nil];
297  EXPECT_EQ(testView.layer.sublayers.count, 0ul);
298  EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(0, 0)));
299 }
300 
301 } // namespace flutter::testing
FlutterMetalTexture asFlutterMetalTexture()
nullable FlutterSurface * fromFlutterMetalTexture:(nonnull const FlutterMetalTexture *texture)
nonnull FlutterSurface * surfaceForSize:(CGSize size)
FlutterBackBufferCache * backBufferCache
void presentSurfaces:atTime:notify:(nonnull NSArray< FlutterSurfacePresentInfo * > *surfaces,[atTime] CFTimeInterval presentationTime,[notify] nullable dispatch_block_t notify)
NSArray< FlutterSurface * > * frontSurfaces
std::vector< FlutterRect > paintRegion
nonnull instancetype init()
TEST(FlutterSurfaceManager, LayerManagement)
bool operator==(const CGRect &lhs, const CGRect &rhs)
static FlutterSurfacePresentInfo * CreatePresentInfo(FlutterSurface *surface, CGPoint offset=CGPointZero, size_t index=0, const std::vector< FlutterRect > &paintRegion={})
static FlutterSurfaceManager * CreateSurfaceManager(TestView *testView)