diff --git a/impeller/renderer/backend/metal/surface_mtl.mm b/impeller/renderer/backend/metal/surface_mtl.mm index f3d5240807485..f23cbfbe13895 100644 --- a/impeller/renderer/backend/metal/surface_mtl.mm +++ b/impeller/renderer/backend/metal/surface_mtl.mm @@ -13,6 +13,10 @@ #include "impeller/renderer/backend/metal/texture_mtl.h" #include "impeller/renderer/render_target.h" +@protocol FlutterMetalDrawable +- (void)flutterPrepareForPresent:(nonnull id)commandBuffer; +@end + namespace impeller { #pragma GCC diagnostic push @@ -254,6 +258,14 @@ id command_buffer = ContextMTL::Cast(context.get()) ->CreateMTLCommandBuffer("Present Waiter Command Buffer"); + + id metal_drawable = + reinterpret_cast>(drawable_); + if ([metal_drawable conformsToProtocol:@protocol(FlutterMetalDrawable)]) { + [(id)metal_drawable + flutterPrepareForPresent:command_buffer]; + } + // If the threads have been merged, or there is a pending frame capture, // then block on cmd buffer scheduling to ensure that the // transaction/capture work correctly. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h b/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h index f6a1108cb52d4..21b63b4a048ac 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h @@ -28,4 +28,16 @@ @end +@protocol MTLCommandBuffer; + +@protocol FlutterMetalDrawable + +/// In order for FlutterMetalLayer to provide back pressure it must have access +/// to the command buffer that is used to render into the drawable to schedule +/// a completion handler. +/// This method must be called before the command buffer is committed. +- (void)flutterPrepareForPresent:(nonnull id)commandBuffer; + +@end + #endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERMETALLAYER_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm b/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm index 94d4c04e675ef..f22dfefb52058 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm @@ -60,6 +60,7 @@ @interface FlutterTexture : NSObject { @property(readonly, nonatomic) id texture; @property(readonly, nonatomic) IOSurface* surface; @property(readwrite, nonatomic) CFTimeInterval presentedTime; +@property(readwrite, atomic) BOOL waitingForCompletion; @end @@ -68,6 +69,7 @@ @implementation FlutterTexture @synthesize texture = _texture; @synthesize surface = _surface; @synthesize presentedTime = _presentedTime; +@synthesize waitingForCompletion; - (instancetype)initWithTexture:(id)texture surface:(IOSurface*)surface { if (self = [super init]) { @@ -79,7 +81,7 @@ - (instancetype)initWithTexture:(id)texture surface:(IOSurface*)surf @end -@interface FlutterDrawable : NSObject { +@interface FlutterDrawable : NSObject { FlutterTexture* _texture; __weak FlutterMetalLayer* _layer; NSUInteger _drawableId; @@ -147,6 +149,14 @@ - (void)presentAfterMinimumDuration:(CFTimeInterval)duration { FML_LOG(WARNING) << "FlutterMetalLayer drawable does not implement presentAfterMinimumDuration:"; } +- (void)flutterPrepareForPresent:(nonnull id)commandBuffer { + FlutterTexture* texture = _texture; + texture.waitingForCompletion = YES; + [commandBuffer addCompletedHandler:^(id buffer) { + texture.waitingForCompletion = NO; + }]; +} + @end @implementation FlutterMetalLayer @@ -283,7 +293,25 @@ - (IOSurface*)createIOSurface { } - (FlutterTexture*)nextTexture { + CFTimeInterval start = CACurrentMediaTime(); + while (true) { + FlutterTexture* texture = [self tryNextTexture]; + if (texture != nil) { + return texture; + } + CFTimeInterval elapsed = CACurrentMediaTime() - start; + if (elapsed > 1.0) { + NSLog(@"Waited %f seconds for a drawable, giving up.", elapsed); + return nil; + } + } +} + +- (FlutterTexture*)tryNextTexture { @synchronized(self) { + if (_front != nil && _front.waitingForCompletion) { + return nil; + } if (_totalTextures < 3) { ++_totalTextures; IOSurface* surface = [self createIOSurface]; @@ -309,21 +337,6 @@ - (FlutterTexture*)nextTexture { surface:surface]; return flutterTexture; } else { - // Make sure raster thread doesn't have too many drawables in flight. - if (_availableTextures.count == 0) { - CFTimeInterval start = CACurrentMediaTime(); - while (_availableTextures.count == 0 && CACurrentMediaTime() - start < 1.0) { - usleep(100); - } - CFTimeInterval elapsed = CACurrentMediaTime() - start; - if (_availableTextures.count == 0) { - NSLog(@"Waited %f seconds for a drawable, giving up.", elapsed); - return nil; - } else { - NSLog(@"Had to wait %f seconds for a drawable", elapsed); - } - } - // Prefer surface that is not in use and has been presented the longest // time ago. // When isInUse is false, the surface is definitely not used by the compositor. @@ -345,7 +358,9 @@ - (FlutterTexture*)nextTexture { res = texture; } } - [_availableTextures removeObject:res]; + if (res != nil) { + [_availableTextures removeObject:res]; + } return res; } } @@ -370,7 +385,6 @@ - (void)presentOnMainThread:(FlutterTexture*)texture { [CATransaction begin]; [CATransaction setDisableActions:YES]; self.contents = texture.surface; - texture.presentedTime = CACurrentMediaTime(); [CATransaction commit]; _displayLink.paused = NO; _displayLinkPauseCountdown = 0; @@ -388,6 +402,7 @@ - (void)presentTexture:(FlutterTexture*)texture { [_availableTextures addObject:_front]; } _front = texture; + texture.presentedTime = CACurrentMediaTime(); if ([NSThread isMainThread]) { [self presentOnMainThread:texture]; } else { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterMetalLayerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterMetalLayerTest.mm index 1265fdd216f11..c877c41d9d9b0 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterMetalLayerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterMetalLayerTest.mm @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import #import #import @@ -236,4 +237,37 @@ - (void)testLayerLimitsDrawableCount { [self removeMetalLayer:layer]; } +- (void)testTimeout { + FlutterMetalLayer* layer = [self addMetalLayer]; + TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer]; + + id drawable = [layer nextDrawable]; + BAIL_IF_NO_DRAWABLE(drawable); + + __block MTLCommandBufferHandler handler; + + id 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)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 diff --git a/shell/platform/darwin/ios/ios_surface_metal_skia.mm b/shell/platform/darwin/ios/ios_surface_metal_skia.mm index d010c181daa9b..f33788deb59ba 100644 --- a/shell/platform/darwin/ios/ios_surface_metal_skia.mm +++ b/shell/platform/darwin/ios/ios_surface_metal_skia.mm @@ -8,6 +8,10 @@ #include "flutter/shell/gpu/gpu_surface_metal_skia.h" #include "flutter/shell/platform/darwin/ios/ios_context_metal_skia.h" +@protocol FlutterMetalDrawable +- (void)flutterPrepareForPresent:(nonnull id)commandBuffer; +@end + namespace flutter { static IOSContextMetalSkia* CastToMetalContext(const std::shared_ptr& context) { @@ -80,10 +84,16 @@ auto command_buffer = fml::scoped_nsprotocol>([[command_queue_ commandBuffer] retain]); + + id metal_drawable = reinterpret_cast>(drawable); + if ([metal_drawable conformsToProtocol:@protocol(FlutterMetalDrawable)]) { + [(id)metal_drawable flutterPrepareForPresent:command_buffer.get()]; + } + [command_buffer.get() commit]; [command_buffer.get() waitUntilScheduled]; - [reinterpret_cast>(drawable) present]; + [metal_drawable present]; return true; }