Flutter Engine
 
Loading...
Searching...
No Matches
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)];
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- (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
#define BAIL_IF_NO_DRAWABLE(drawable)
VkSurfaceKHR surface
Definition main.cc:65
FlView * view
const gchar FlBinaryMessengerMessageHandler handler
nullable id< CAMetalDrawable > nextDrawable()
FlutterMetalLayer * _layer
IOSurfaceRef _presentedSurface
FlTexture * texture
const size_t start