Flutter iOS Embedder
FlutterMetalLayerTest.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 #import <OCMock/OCMock.h>
9 #import <QuartzCore/QuartzCore.h>
10 #import <XCTest/XCTest.h>
11 
12 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
13 
14 @interface FlutterMetalLayerTest : XCTestCase
15 @end
16 
17 @interface TestFlutterMetalLayerView : UIView
18 @end
19 
20 @implementation TestFlutterMetalLayerView
21 
22 + (Class)layerClass {
23  return [FlutterMetalLayer class];
24 }
25 
26 @end
27 
28 /// A fake compositor that simulates presenting layer surface by increasing
29 /// and decreasing IOSurface use count.
30 @interface TestCompositor : NSObject {
32  IOSurfaceRef _presentedSurface;
33 }
34 @end
35 
36 @implementation TestCompositor
37 
38 - (instancetype)initWithLayer:(FlutterMetalLayer*)layer {
39  self = [super init];
40  if (self) {
41  self->_layer = layer;
42  }
43  return self;
44 }
45 
46 /// Increment use count of currently presented surface and decrement use count
47 /// of previously presented surface.
48 - (void)commitTransaction {
49  IOSurfaceRef surface = (__bridge IOSurfaceRef)self->_layer.contents;
50  if (self->_presentedSurface) {
51  IOSurfaceDecrementUseCount(self->_presentedSurface);
52  }
53  IOSurfaceIncrementUseCount(surface);
54  self->_presentedSurface = surface;
55 }
56 
57 - (void)dealloc {
58  if (self->_presentedSurface) {
59  IOSurfaceDecrementUseCount(self->_presentedSurface);
60  }
61 }
62 
63 @end
64 
65 @implementation FlutterMetalLayerTest
66 
67 - (FlutterMetalLayer*)addMetalLayer {
69  [[TestFlutterMetalLayerView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
70  FlutterMetalLayer* layer = (FlutterMetalLayer*)view.layer;
71  layer.drawableSize = CGSizeMake(100, 100);
72  return layer;
73 }
74 
75 - (void)removeMetalLayer:(FlutterMetalLayer*)layer {
76 }
77 
78 // For unknown reason sometimes CI fails to create IOSurface. Bail out
79 // to prevent flakiness.
80 #define BAIL_IF_NO_DRAWABLE(drawable) \
81  if (drawable == nil) { \
82  [FlutterLogger logError:@"Could not allocate drawable"]; \
83  return; \
84  }
85 
86 - (void)testFlip {
87  FlutterMetalLayer* layer = [self addMetalLayer];
88  TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
89 
90  id<MTLTexture> t1, t2, t3;
91 
92  id<CAMetalDrawable> drawable = [layer nextDrawable];
93  BAIL_IF_NO_DRAWABLE(drawable);
94  t1 = drawable.texture;
95  [drawable present];
96  [compositor commitTransaction];
97 
98  drawable = [layer nextDrawable];
99  BAIL_IF_NO_DRAWABLE(drawable);
100  t2 = drawable.texture;
101  [drawable present];
102  [compositor commitTransaction];
103 
104  drawable = [layer nextDrawable];
105  BAIL_IF_NO_DRAWABLE(drawable);
106  t3 = drawable.texture;
107  [drawable present];
108  [compositor commitTransaction];
109 
110  // If there was no frame drop, layer should return oldest presented
111  // texture.
112 
113  drawable = [layer nextDrawable];
114  XCTAssertEqual(drawable.texture, t1);
115 
116  [drawable present];
117  [compositor commitTransaction];
118 
119  drawable = [layer nextDrawable];
120  XCTAssertEqual(drawable.texture, t2);
121  [drawable present];
122  [compositor commitTransaction];
123 
124  drawable = [layer nextDrawable];
125  XCTAssertEqual(drawable.texture, t3);
126  [drawable present];
127  [compositor commitTransaction];
128 
129  drawable = [layer nextDrawable];
130  XCTAssertEqual(drawable.texture, t1);
131  [drawable present];
132 
133  [self removeMetalLayer:layer];
134 }
135 
136 - (void)testFlipWithDroppedFrame {
137  FlutterMetalLayer* layer = [self addMetalLayer];
138  TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
139 
140  id<MTLTexture> t1, t2, t3;
141 
142  id<CAMetalDrawable> drawable = [layer nextDrawable];
143  BAIL_IF_NO_DRAWABLE(drawable);
144  t1 = drawable.texture;
145  [drawable present];
146  [compositor commitTransaction];
147  XCTAssertTrue(IOSurfaceIsInUse(t1.iosurface));
148 
149  drawable = [layer nextDrawable];
150  BAIL_IF_NO_DRAWABLE(drawable);
151  t2 = drawable.texture;
152  [drawable present];
153  [compositor commitTransaction];
154 
155  drawable = [layer nextDrawable];
156  BAIL_IF_NO_DRAWABLE(drawable);
157  t3 = drawable.texture;
158  [drawable present];
159  [compositor commitTransaction];
160 
161  // Simulate compositor holding on to t3 for a while.
162  IOSurfaceIncrementUseCount(t3.iosurface);
163 
164  // Here the drawable is presented, but immediately replaced by another drawable
165  // (before the compositor has a chance to pick it up). This should result
166  // in same drawable returned in next call to nextDrawable.
167  drawable = [layer nextDrawable];
168  XCTAssertEqual(drawable.texture, t1);
169  XCTAssertFalse(IOSurfaceIsInUse(drawable.texture.iosurface));
170  [drawable present];
171 
172  drawable = [layer nextDrawable];
173  XCTAssertEqual(drawable.texture, t2);
174  [drawable present];
175  [compositor commitTransaction];
176 
177  // Next drawable should be t1, since it was never picked up by compositor.
178  drawable = [layer nextDrawable];
179  XCTAssertEqual(drawable.texture, t1);
180 
181  IOSurfaceDecrementUseCount(t3.iosurface);
182 
183  [self removeMetalLayer:layer];
184 }
185 
186 - (void)testDroppedDrawableReturnsTextureToPool {
187  FlutterMetalLayer* layer = [self addMetalLayer];
188  // FlutterMetalLayer will keep creating new textures until it has 3.
189  @autoreleasepool {
190  for (int i = 0; i < 3; ++i) {
191  id<CAMetalDrawable> drawable = [layer nextDrawable];
192  BAIL_IF_NO_DRAWABLE(drawable);
193  }
194  }
195  id<MTLTexture> texture;
196  {
197  @autoreleasepool {
198  id<CAMetalDrawable> drawable = [layer nextDrawable];
199  XCTAssertNotNil(drawable);
200  texture = drawable.texture;
201  // Dropping the drawable must return texture to pool, so
202  // next drawable should return the same texture.
203  }
204  }
205  {
206  id<CAMetalDrawable> drawable = [layer nextDrawable];
207  XCTAssertEqual(texture, drawable.texture);
208  }
209 
210  [self removeMetalLayer:layer];
211 }
212 
213 - (void)testLayerLimitsDrawableCount {
214  FlutterMetalLayer* layer = [self addMetalLayer];
215 
216  id<CAMetalDrawable> d1 = [layer nextDrawable];
218  id<CAMetalDrawable> d2 = [layer nextDrawable];
220  id<CAMetalDrawable> d3 = [layer nextDrawable];
222  XCTAssertNotNil(d3);
223 
224  // Layer should not return more than 3 drawables.
225  id<CAMetalDrawable> d4 = [layer nextDrawable];
226  XCTAssertNil(d4);
227 
228  [d1 present];
229 
230  // Still no drawable, until the front buffer returns to pool
231  id<CAMetalDrawable> d5 = [layer nextDrawable];
232  XCTAssertNil(d5);
233 
234  [d2 present];
235  id<CAMetalDrawable> d6 = [layer nextDrawable];
236  XCTAssertNotNil(d6);
237 
238  [self removeMetalLayer:layer];
239 }
240 
241 - (void)testTimeout {
242  FlutterMetalLayer* layer = [self addMetalLayer];
243  TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
244 
245  id<CAMetalDrawable> drawable = [layer nextDrawable];
246  BAIL_IF_NO_DRAWABLE(drawable);
247 
248  __block MTLCommandBufferHandler handler;
249 
250  id<MTLCommandBuffer> mockCommandBuffer = OCMProtocolMock(@protocol(MTLCommandBuffer));
251  OCMStub([mockCommandBuffer addCompletedHandler:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
252  MTLCommandBufferHandler handlerOnStack;
253  [invocation getArgument:&handlerOnStack atIndex:2];
254  // Required to copy stack block to heap.
255  handler = handlerOnStack;
256  });
257 
258  [(id<FlutterMetalDrawable>)drawable flutterPrepareForPresent:mockCommandBuffer];
259  [drawable present];
260  [compositor commitTransaction];
261 
262  // Drawable will not be available until the command buffer completes.
263  drawable = [layer nextDrawable];
264  XCTAssertNil(drawable);
265 
266  handler(mockCommandBuffer);
267 
268  drawable = [layer nextDrawable];
269  XCTAssertNotNil(drawable);
270 
271  [self removeMetalLayer:layer];
272 }
273 
274 - (void)testDealloc {
275  __weak FlutterMetalLayer* weakLayer;
276  @autoreleasepool {
277  FlutterMetalLayer* layer = [self addMetalLayer];
278  weakLayer = layer;
279  TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
280 
281  id<CAMetalDrawable> drawable = [layer nextDrawable];
282  BAIL_IF_NO_DRAWABLE(drawable);
283  [drawable present];
284  [compositor commitTransaction];
285 
286  [self removeMetalLayer:layer];
287  }
288  CFTimeInterval start = CACurrentMediaTime();
289  while (weakLayer != nil && CACurrentMediaTime() - start < 1) {
290  // Deallocating the layer after removing is not synchronous.
291  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
292  }
293 
294  XCTAssertNil(weakLayer);
295 }
296 
297 @end
#define BAIL_IF_NO_DRAWABLE(drawable)
nullable id< CAMetalDrawable > nextDrawable()
FlutterMetalLayer * _layer
IOSurfaceRef _presentedSurface