Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions impeller/renderer/backend/metal/surface_mtl.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
#include "impeller/renderer/backend/metal/texture_mtl.h"
#include "impeller/renderer/render_target.h"

@protocol FlutterMetalDrawable <MTLDrawable>
- (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer;
@end

namespace impeller {

#pragma GCC diagnostic push
Expand Down Expand Up @@ -254,6 +258,14 @@
id<MTLCommandBuffer> command_buffer =
ContextMTL::Cast(context.get())
->CreateMTLCommandBuffer("Present Waiter Command Buffer");

id<CAMetalDrawable> metal_drawable =
reinterpret_cast<id<CAMetalDrawable>>(drawable_);
if ([metal_drawable conformsToProtocol:@protocol(FlutterMetalDrawable)]) {
[(id<FlutterMetalDrawable>)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.
Expand Down
12 changes: 12 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@

@end

@protocol MTLCommandBuffer;

@protocol FlutterMetalDrawable <CAMetalDrawable>

/// 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<MTLCommandBuffer>)commandBuffer;

@end

#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERMETALLAYER_H_
51 changes: 33 additions & 18 deletions shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ @interface FlutterTexture : NSObject {
@property(readonly, nonatomic) id<MTLTexture> texture;
@property(readonly, nonatomic) IOSurface* surface;
@property(readwrite, nonatomic) CFTimeInterval presentedTime;
@property(readwrite, atomic) BOOL waitingForCompletion;

@end

Expand All @@ -68,6 +69,7 @@ @implementation FlutterTexture
@synthesize texture = _texture;
@synthesize surface = _surface;
@synthesize presentedTime = _presentedTime;
@synthesize waitingForCompletion;

- (instancetype)initWithTexture:(id<MTLTexture>)texture surface:(IOSurface*)surface {
if (self = [super init]) {
Expand All @@ -79,7 +81,7 @@ - (instancetype)initWithTexture:(id<MTLTexture>)texture surface:(IOSurface*)surf

@end

@interface FlutterDrawable : NSObject <CAMetalDrawable> {
@interface FlutterDrawable : NSObject <FlutterMetalDrawable> {
FlutterTexture* _texture;
__weak FlutterMetalLayer* _layer;
NSUInteger _drawableId;
Expand Down Expand Up @@ -147,6 +149,14 @@ - (void)presentAfterMinimumDuration:(CFTimeInterval)duration {
FML_LOG(WARNING) << "FlutterMetalLayer drawable does not implement presentAfterMinimumDuration:";
}

- (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer {
FlutterTexture* texture = _texture;
texture.waitingForCompletion = YES;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
texture.waitingForCompletion = NO;
}];
}

@end

@implementation FlutterMetalLayer
Expand Down Expand Up @@ -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];
Expand All @@ -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.
Expand All @@ -345,7 +358,9 @@ - (FlutterTexture*)nextTexture {
res = texture;
}
}
[_availableTextures removeObject:res];
if (res != nil) {
[_availableTextures removeObject:res];
}
return res;
}
}
Expand All @@ -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;
Expand All @@ -388,6 +402,7 @@ - (void)presentTexture:(FlutterTexture*)texture {
[_availableTextures addObject:_front];
}
_front = texture;
texture.presentedTime = CACurrentMediaTime();
if ([NSThread isMainThread]) {
[self presentOnMainThread:texture];
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

#import <Metal/Metal.h>
#import <OCMock/OCMock.h>
#import <QuartzCore/QuartzCore.h>
#import <XCTest/XCTest.h>

Expand Down Expand Up @@ -236,4 +237,37 @@ - (void)testLayerLimitsDrawableCount {
[self removeMetalLayer:layer];
}

- (void)testTimeout {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs more thought, I haven't actually looked at how to grab the usage flag in the test yet

Copy link
Member

@knopp knopp Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test fails because isInUse=true doesn't prevent layer from returning the surface.

isInUse=false: Compositor is definitely not using the surface. This is useful to reuse surface immediately after a dropped frame.
isInUse=true: Compositor may be using the surface. if there is no surface with isInUse=false, return one with isUnUse=true that was presented longest time ago.

The blocking in this PR is performed when front surface command buffer has not been completed. This is different from compositor using the surface. The test will likely need to call flutterPrepareForPresent: with command buffer and then test if nextSurface blocks while the command buffer waits for completion.

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
12 changes: 11 additions & 1 deletion shell/platform/darwin/ios/ios_surface_metal_skia.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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 <MTLDrawable>
- (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer;
@end

namespace flutter {

static IOSContextMetalSkia* CastToMetalContext(const std::shared_ptr<IOSContext>& context) {
Expand Down Expand Up @@ -80,10 +84,16 @@

auto command_buffer =
fml::scoped_nsprotocol<id<MTLCommandBuffer>>([[command_queue_ commandBuffer] retain]);

id<CAMetalDrawable> metal_drawable = reinterpret_cast<id<CAMetalDrawable>>(drawable);
if ([metal_drawable conformsToProtocol:@protocol(FlutterMetalDrawable)]) {
[(id<FlutterMetalDrawable>)metal_drawable flutterPrepareForPresent:command_buffer.get()];
}

[command_buffer.get() commit];
[command_buffer.get() waitUntilScheduled];

[reinterpret_cast<id<CAMetalDrawable>>(drawable) present];
[metal_drawable present];
return true;
}

Expand Down