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