| 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"
11 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.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 {
30 FlutterMetalLayer* _layer;
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 {
67 TestFlutterMetalLayerView* view =
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];
216 BAIL_IF_NO_DRAWABLE(d1);
217 id<CAMetalDrawable> d2 = [layer nextDrawable];
218 BAIL_IF_NO_DRAWABLE(d2);
219 id<CAMetalDrawable> d3 = [layer nextDrawable];
220 BAIL_IF_NO_DRAWABLE(d3);
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