blob: c877c41d9d9b052bf92d4edf0f3ecd917f7b55a5 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Metal/Metal.h>
#import <OCMock/OCMock.h>
#import <QuartzCore/QuartzCore.h>
#import <XCTest/XCTest.h>
#include "flutter/fml/logging.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h"
@interface FlutterMetalLayerTest : XCTestCase
@end
@interface TestFlutterMetalLayerView : UIView
@end
@implementation TestFlutterMetalLayerView
+ (Class)layerClass {
return [FlutterMetalLayer class];
}
@end
/// A fake compositor that simulates presenting layer surface by increasing
/// and decreasing IOSurface use count.
@interface TestCompositor : NSObject {
FlutterMetalLayer* _layer;
IOSurfaceRef _presentedSurface;
}
@end
@implementation TestCompositor
- (instancetype)initWithLayer:(FlutterMetalLayer*)layer {
self = [super init];
if (self) {
self->_layer = layer;
}
return self;
}
/// Increment use count of currently presented surface and decrement use count
/// of previously presented surface.
- (void)commitTransaction {
IOSurfaceRef surface = (__bridge IOSurfaceRef)self->_layer.contents;
if (self->_presentedSurface) {
IOSurfaceDecrementUseCount(self->_presentedSurface);
}
IOSurfaceIncrementUseCount(surface);
self->_presentedSurface = surface;
}
- (void)dealloc {
if (self->_presentedSurface) {
IOSurfaceDecrementUseCount(self->_presentedSurface);
}
}
@end
@implementation FlutterMetalLayerTest
- (FlutterMetalLayer*)addMetalLayer {
TestFlutterMetalLayerView* view =
[[TestFlutterMetalLayerView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
FlutterMetalLayer* layer = (FlutterMetalLayer*)view.layer;
layer.drawableSize = CGSizeMake(100, 100);
return layer;
}
- (void)removeMetalLayer:(FlutterMetalLayer*)layer {
}
// For unknown reason sometimes CI fails to create IOSurface. Bail out
// to prevent flakiness.
#define BAIL_IF_NO_DRAWABLE(drawable) \
if (drawable == nil) { \
FML_LOG(ERROR) << "Could not allocate drawable"; \
return; \
}
- (void)testFlip {
FlutterMetalLayer* layer = [self addMetalLayer];
TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
id<MTLTexture> t1, t2, t3;
id<CAMetalDrawable> drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
t1 = drawable.texture;
[drawable present];
[compositor commitTransaction];
drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
t2 = drawable.texture;
[drawable present];
[compositor commitTransaction];
drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
t3 = drawable.texture;
[drawable present];
[compositor commitTransaction];
// If there was no frame drop, layer should return oldest presented
// texture.
drawable = [layer nextDrawable];
XCTAssertEqual(drawable.texture, t1);
[drawable present];
[compositor commitTransaction];
drawable = [layer nextDrawable];
XCTAssertEqual(drawable.texture, t2);
[drawable present];
[compositor commitTransaction];
drawable = [layer nextDrawable];
XCTAssertEqual(drawable.texture, t3);
[drawable present];
[compositor commitTransaction];
drawable = [layer nextDrawable];
XCTAssertEqual(drawable.texture, t1);
[drawable present];
[self removeMetalLayer:layer];
}
- (void)testFlipWithDroppedFrame {
FlutterMetalLayer* layer = [self addMetalLayer];
TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
id<MTLTexture> t1, t2, t3;
id<CAMetalDrawable> drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
t1 = drawable.texture;
[drawable present];
[compositor commitTransaction];
XCTAssertTrue(IOSurfaceIsInUse(t1.iosurface));
drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
t2 = drawable.texture;
[drawable present];
[compositor commitTransaction];
drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
t3 = drawable.texture;
[drawable present];
[compositor commitTransaction];
// Simulate compositor holding on to t3 for a while.
IOSurfaceIncrementUseCount(t3.iosurface);
// Here the drawable is presented, but immediately replaced by another drawable
// (before the compositor has a chance to pick it up). This should result
// in same drawable returned in next call to nextDrawable.
drawable = [layer nextDrawable];
XCTAssertEqual(drawable.texture, t1);
XCTAssertFalse(IOSurfaceIsInUse(drawable.texture.iosurface));
[drawable present];
drawable = [layer nextDrawable];
XCTAssertEqual(drawable.texture, t2);
[drawable present];
[compositor commitTransaction];
// Next drawable should be t1, since it was never picked up by compositor.
drawable = [layer nextDrawable];
XCTAssertEqual(drawable.texture, t1);
IOSurfaceDecrementUseCount(t3.iosurface);
[self removeMetalLayer:layer];
}
- (void)testDroppedDrawableReturnsTextureToPool {
FlutterMetalLayer* layer = [self addMetalLayer];
// FlutterMetalLayer will keep creating new textures until it has 3.
@autoreleasepool {
for (int i = 0; i < 3; ++i) {
id<CAMetalDrawable> drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
}
}
id<MTLTexture> texture;
{
@autoreleasepool {
id<CAMetalDrawable> drawable = [layer nextDrawable];
XCTAssertNotNil(drawable);
texture = (id<MTLTexture>)drawable.texture;
// Dropping the drawable must return texture to pool, so
// next drawable should return the same texture.
}
}
{
id<CAMetalDrawable> drawable = [layer nextDrawable];
XCTAssertEqual(texture, drawable.texture);
}
[self removeMetalLayer:layer];
}
- (void)testLayerLimitsDrawableCount {
FlutterMetalLayer* layer = [self addMetalLayer];
id<CAMetalDrawable> d1 = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(d1);
id<CAMetalDrawable> d2 = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(d2);
id<CAMetalDrawable> d3 = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(d3);
XCTAssertNotNil(d3);
// Layer should not return more than 3 drawables.
id<CAMetalDrawable> d4 = [layer nextDrawable];
XCTAssertNil(d4);
[d1 present];
// Still no drawable, until the front buffer returns to pool
id<CAMetalDrawable> d5 = [layer nextDrawable];
XCTAssertNil(d5);
[d2 present];
id<CAMetalDrawable> d6 = [layer nextDrawable];
XCTAssertNotNil(d6);
[self removeMetalLayer:layer];
}
- (void)testTimeout {
FlutterMetalLayer* layer = [self addMetalLayer];
TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
id<CAMetalDrawable> drawable = [layer nextDrawable];
BAIL_IF_NO_DRAWABLE(drawable);
__block MTLCommandBufferHandler handler;
id<MTLCommandBuffer> mockCommandBuffer = OCMProtocolMock(@protocol(MTLCommandBuffer));
OCMStub([mockCommandBuffer addCompletedHandler:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
MTLCommandBufferHandler handlerOnStack;
[invocation getArgument:&handlerOnStack atIndex:2];
// Required to copy stack block to heap.
handler = handlerOnStack;
});
[(id<FlutterMetalDrawable>)drawable flutterPrepareForPresent:mockCommandBuffer];
[drawable present];
[compositor commitTransaction];
// Drawable will not be available until the command buffer completes.
drawable = [layer nextDrawable];
XCTAssertNil(drawable);
handler(mockCommandBuffer);
drawable = [layer nextDrawable];
XCTAssertNotNil(drawable);
[self removeMetalLayer:layer];
}
@end