Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 5548b4b

Browse files
author
Jonah Williams
authored
[iOS] Ensure FlutterMetalLayer has correct backpressure. (#50486)
See also flutter/flutter#140901 . We were not accounting for GPU backpressure in the FML, this applies the patch from @knopp to track this.
1 parent aae6dac commit 5548b4b

File tree

5 files changed

+102
-19
lines changed

5 files changed

+102
-19
lines changed

impeller/renderer/backend/metal/surface_mtl.mm

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
#include "impeller/renderer/backend/metal/texture_mtl.h"
1414
#include "impeller/renderer/render_target.h"
1515

16+
@protocol FlutterMetalDrawable <MTLDrawable>
17+
- (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer;
18+
@end
19+
1620
namespace impeller {
1721

1822
#pragma GCC diagnostic push
@@ -254,6 +258,14 @@
254258
id<MTLCommandBuffer> command_buffer =
255259
ContextMTL::Cast(context.get())
256260
->CreateMTLCommandBuffer("Present Waiter Command Buffer");
261+
262+
id<CAMetalDrawable> metal_drawable =
263+
reinterpret_cast<id<CAMetalDrawable>>(drawable_);
264+
if ([metal_drawable conformsToProtocol:@protocol(FlutterMetalDrawable)]) {
265+
[(id<FlutterMetalDrawable>)metal_drawable
266+
flutterPrepareForPresent:command_buffer];
267+
}
268+
257269
// If the threads have been merged, or there is a pending frame capture,
258270
// then block on cmd buffer scheduling to ensure that the
259271
// transaction/capture work correctly.

shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,16 @@
2828

2929
@end
3030

31+
@protocol MTLCommandBuffer;
32+
33+
@protocol FlutterMetalDrawable <CAMetalDrawable>
34+
35+
/// In order for FlutterMetalLayer to provide back pressure it must have access
36+
/// to the command buffer that is used to render into the drawable to schedule
37+
/// a completion handler.
38+
/// This method must be called before the command buffer is committed.
39+
- (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer;
40+
41+
@end
42+
3143
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERMETALLAYER_H_

shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ @interface FlutterTexture : NSObject {
6060
@property(readonly, nonatomic) id<MTLTexture> texture;
6161
@property(readonly, nonatomic) IOSurface* surface;
6262
@property(readwrite, nonatomic) CFTimeInterval presentedTime;
63+
@property(readwrite, atomic) BOOL waitingForCompletion;
6364

6465
@end
6566

@@ -68,6 +69,7 @@ @implementation FlutterTexture
6869
@synthesize texture = _texture;
6970
@synthesize surface = _surface;
7071
@synthesize presentedTime = _presentedTime;
72+
@synthesize waitingForCompletion;
7173

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

8082
@end
8183

82-
@interface FlutterDrawable : NSObject <CAMetalDrawable> {
84+
@interface FlutterDrawable : NSObject <FlutterMetalDrawable> {
8385
FlutterTexture* _texture;
8486
__weak FlutterMetalLayer* _layer;
8587
NSUInteger _drawableId;
@@ -147,6 +149,14 @@ - (void)presentAfterMinimumDuration:(CFTimeInterval)duration {
147149
FML_LOG(WARNING) << "FlutterMetalLayer drawable does not implement presentAfterMinimumDuration:";
148150
}
149151

152+
- (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer {
153+
FlutterTexture* texture = _texture;
154+
texture.waitingForCompletion = YES;
155+
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
156+
texture.waitingForCompletion = NO;
157+
}];
158+
}
159+
150160
@end
151161

152162
@implementation FlutterMetalLayer
@@ -283,7 +293,25 @@ - (IOSurface*)createIOSurface {
283293
}
284294

285295
- (FlutterTexture*)nextTexture {
296+
CFTimeInterval start = CACurrentMediaTime();
297+
while (true) {
298+
FlutterTexture* texture = [self tryNextTexture];
299+
if (texture != nil) {
300+
return texture;
301+
}
302+
CFTimeInterval elapsed = CACurrentMediaTime() - start;
303+
if (elapsed > 1.0) {
304+
NSLog(@"Waited %f seconds for a drawable, giving up.", elapsed);
305+
return nil;
306+
}
307+
}
308+
}
309+
310+
- (FlutterTexture*)tryNextTexture {
286311
@synchronized(self) {
312+
if (_front != nil && _front.waitingForCompletion) {
313+
return nil;
314+
}
287315
if (_totalTextures < 3) {
288316
++_totalTextures;
289317
IOSurface* surface = [self createIOSurface];
@@ -309,21 +337,6 @@ - (FlutterTexture*)nextTexture {
309337
surface:surface];
310338
return flutterTexture;
311339
} else {
312-
// Make sure raster thread doesn't have too many drawables in flight.
313-
if (_availableTextures.count == 0) {
314-
CFTimeInterval start = CACurrentMediaTime();
315-
while (_availableTextures.count == 0 && CACurrentMediaTime() - start < 1.0) {
316-
usleep(100);
317-
}
318-
CFTimeInterval elapsed = CACurrentMediaTime() - start;
319-
if (_availableTextures.count == 0) {
320-
NSLog(@"Waited %f seconds for a drawable, giving up.", elapsed);
321-
return nil;
322-
} else {
323-
NSLog(@"Had to wait %f seconds for a drawable", elapsed);
324-
}
325-
}
326-
327340
// Prefer surface that is not in use and has been presented the longest
328341
// time ago.
329342
// When isInUse is false, the surface is definitely not used by the compositor.
@@ -345,7 +358,9 @@ - (FlutterTexture*)nextTexture {
345358
res = texture;
346359
}
347360
}
348-
[_availableTextures removeObject:res];
361+
if (res != nil) {
362+
[_availableTextures removeObject:res];
363+
}
349364
return res;
350365
}
351366
}
@@ -370,7 +385,6 @@ - (void)presentOnMainThread:(FlutterTexture*)texture {
370385
[CATransaction begin];
371386
[CATransaction setDisableActions:YES];
372387
self.contents = texture.surface;
373-
texture.presentedTime = CACurrentMediaTime();
374388
[CATransaction commit];
375389
_displayLink.paused = NO;
376390
_displayLinkPauseCountdown = 0;
@@ -388,6 +402,7 @@ - (void)presentTexture:(FlutterTexture*)texture {
388402
[_availableTextures addObject:_front];
389403
}
390404
_front = texture;
405+
texture.presentedTime = CACurrentMediaTime();
391406
if ([NSThread isMainThread]) {
392407
[self presentOnMainThread:texture];
393408
} else {

shell/platform/darwin/ios/framework/Source/FlutterMetalLayerTest.mm

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
#import <Metal/Metal.h>
6+
#import <OCMock/OCMock.h>
67
#import <QuartzCore/QuartzCore.h>
78
#import <XCTest/XCTest.h>
89

@@ -236,4 +237,37 @@ - (void)testLayerLimitsDrawableCount {
236237
[self removeMetalLayer:layer];
237238
}
238239

240+
- (void)testTimeout {
241+
FlutterMetalLayer* layer = [self addMetalLayer];
242+
TestCompositor* compositor = [[TestCompositor alloc] initWithLayer:layer];
243+
244+
id<CAMetalDrawable> drawable = [layer nextDrawable];
245+
BAIL_IF_NO_DRAWABLE(drawable);
246+
247+
__block MTLCommandBufferHandler handler;
248+
249+
id<MTLCommandBuffer> mockCommandBuffer = OCMProtocolMock(@protocol(MTLCommandBuffer));
250+
OCMStub([mockCommandBuffer addCompletedHandler:OCMOCK_ANY]).andDo(^(NSInvocation* invocation) {
251+
MTLCommandBufferHandler handlerOnStack;
252+
[invocation getArgument:&handlerOnStack atIndex:2];
253+
// Required to copy stack block to heap.
254+
handler = handlerOnStack;
255+
});
256+
257+
[(id<FlutterMetalDrawable>)drawable flutterPrepareForPresent:mockCommandBuffer];
258+
[drawable present];
259+
[compositor commitTransaction];
260+
261+
// Drawable will not be available until the command buffer completes.
262+
drawable = [layer nextDrawable];
263+
XCTAssertNil(drawable);
264+
265+
handler(mockCommandBuffer);
266+
267+
drawable = [layer nextDrawable];
268+
XCTAssertNotNil(drawable);
269+
270+
[self removeMetalLayer:layer];
271+
}
272+
239273
@end

shell/platform/darwin/ios/ios_surface_metal_skia.mm

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
#include "flutter/shell/gpu/gpu_surface_metal_skia.h"
99
#include "flutter/shell/platform/darwin/ios/ios_context_metal_skia.h"
1010

11+
@protocol FlutterMetalDrawable <MTLDrawable>
12+
- (void)flutterPrepareForPresent:(nonnull id<MTLCommandBuffer>)commandBuffer;
13+
@end
14+
1115
namespace flutter {
1216

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

8185
auto command_buffer =
8286
fml::scoped_nsprotocol<id<MTLCommandBuffer>>([[command_queue_ commandBuffer] retain]);
87+
88+
id<CAMetalDrawable> metal_drawable = reinterpret_cast<id<CAMetalDrawable>>(drawable);
89+
if ([metal_drawable conformsToProtocol:@protocol(FlutterMetalDrawable)]) {
90+
[(id<FlutterMetalDrawable>)metal_drawable flutterPrepareForPresent:command_buffer.get()];
91+
}
92+
8393
[command_buffer.get() commit];
8494
[command_buffer.get() waitUntilScheduled];
8595

86-
[reinterpret_cast<id<CAMetalDrawable>>(drawable) present];
96+
[metal_drawable present];
8797
return true;
8898
}
8999

0 commit comments

Comments
 (0)