| // 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 |