| 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 "flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h"
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 {
31 FlutterMetalLayer* _layer;
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 {
68 TestFlutterMetalLayerView* view =
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];
217 BAIL_IF_NO_DRAWABLE(d1);
218 id<CAMetalDrawable> d2 = [layer nextDrawable];
219 BAIL_IF_NO_DRAWABLE(d2);
220 id<CAMetalDrawable> d3 = [layer nextDrawable];
221 BAIL_IF_NO_DRAWABLE(d3);
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 - (void)testResizeAndPresent {
298 FlutterMetalLayer* layer = [self addMetalLayer];
299 TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
300
301 id<CAMetalDrawable> oldSizeDrawable1 = [layer nextDrawable];
302 BAIL_IF_NO_DRAWABLE(oldSizeDrawable1);
303 id<CAMetalDrawable> oldSizeDrawable2 = [layer nextDrawable];
304 BAIL_IF_NO_DRAWABLE(oldSizeDrawable2);
305
306 const CGFloat newSize = 200;
307 layer.drawableSize = CGSizeMake(newSize, newSize);
308
309 // After resizing, present the drawables that were allocated using the old size.
310 [oldSizeDrawable1 present];
311 [compositor commitTransaction];
312 [oldSizeDrawable2 present];
313 [compositor commitTransaction];
314
315 // Verify that textures with the old size have been removed from the layer.
316 for (int i = 0; i < 4; i++) {
317 id<CAMetalDrawable> drawable = [layer nextDrawable];
318 [drawable present];
319 [compositor commitTransaction];
320 XCTAssertEqual(drawable.texture.width, newSize);
321 }
322
323 [self removeMetalLayer:layer];
324 }
325
326 @end
FlutterMetalLayer.h
BAIL_IF_NO_DRAWABLE
#define BAIL_IF_NO_DRAWABLE(drawable)
Definition: FlutterMetalLayerTest.mm:80
self
return self
Definition: FlutterTextureRegistryRelay.mm:19
FlutterMetalLayer
Definition: FlutterMetalLayer.h:16
-[FlutterMetalLayer nextDrawable]
nullable id< CAMetalDrawable > nextDrawable()
Definition: FlutterMetalLayer.mm:395
FlutterMetalLayer::drawableSize
CGSize drawableSize
Definition: FlutterMetalLayer.h:24
FlutterMetalLayerTest
Definition: FlutterMetalLayerTest.mm:15
TestCompositor
Definition: FlutterMetalLayerTest.mm:30
TestCompositor::_layer
FlutterMetalLayer * _layer
Definition: FlutterMetalLayerTest.mm:31
TestCompositor::_presentedSurface
IOSurfaceRef _presentedSurface
Definition: FlutterMetalLayerTest.mm:32
TestFlutterMetalLayerView
Definition: FlutterMetalLayerTest.mm:18
FlutterMetalDrawable-p
Definition: FlutterMetalLayer.h:38