-
Notifications
You must be signed in to change notification settings - Fork 6k
Add FlutterMetalLayer as optional alternative to CAMetalLayer #48226
Conversation
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie or stuartmorgan on the #hackers channel in Chat (don't just cc them here, they won't see it! Use Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
- (void)presentOnMainThread:(FlutterDrawable*)drawable { | ||
// This is needed otherwise frame gets skipped on touch begin / end. Go figure. | ||
// Might also be placebo | ||
[self setNeedsDisplay]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be fixed in our existing CAtransaction code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
err, DisplayLink
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you refering to [self setNeedsDisplay]
probably makes no difference for metal layer. I'm not even sure it makes difference here. It seems to have helped with frameskip on touch with 120hz display locked to 60hz, but that was several prototypes ago, on iOS 16 and without constant CADisplayLink on main thread. Thinking of it, the behavior might have been affected by touch rate correction displaylink (which is on main thread and enabled only during touch). I left it there just in case, but might be just placebo and needs to be retested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause FlutterView
to take a screenshot...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was removed from the final PR, but since the layer is backed by IOSurface I don't think it has associated CGContextRef
, so I'd assume [FlutterView drawLayer:inContext:]
should receive a nil context? Maybe I'm wrong about this.
return nil; | ||
} | ||
|
||
- (void)presentOnMainThread:(FlutterDrawable*)drawable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I tried something like this with the existing drawable/metal layer a few months back. What I found is that there would occassinally be huge amounts of latency.
Does [CATransaction setDisableActions:YES]
works to prevent the transaction on background thrread state issues? If so, could we not use this to present from a background thread with the regular drawable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I don't really understand how writing our own Metal layer solves this problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about the question? I wasn't able to get CAMetalLayer
present from background thread with transaction smoothly (that's the first graph - with CATransaction disabled, which helped somewhat, but not completely).
The second graph is FlutterMetalLayer
(from the PR), in same app, presenting drawables from background thread with CATransaction at consistent 120Hz.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you're dispatching to the main thread to handle the presentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am. But we don't really know what [CAMetalDrawable present]
does underneath. Apparently it's not just setting layer contents to underlying surface, otherwise disabling the transaction would fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but then I don't really understand how having our own metal layer helps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you set IOSurface (that backs texture that backs drawable) to layer contents, compositor will pick it up and display after next vsync. If you do it within transaction, it will be synchronized with all other UIKit updates. We've been doing this on macOS for couple of years now.
If CAMetalLayer
let us do the same thing, there'd be no need for custom metal layer. But it doesn't. It's full of magic that lets it display content outside of transactions, at custom time, etc, but you can't simply set layer content to the drawable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ack. I might suggest that you find a way to implement this via some opt-in Plist flag or other option that allows for an extended period of testing. But if this solves them problems we've been having with presentation on iOS, I'm all for it!
2030cf7
to
bc70a02
Compare
- (void)setPixelFormat:(MTLPixelFormat)pixelFormat { | ||
_pixelFormat = pixelFormat; | ||
|
||
// FlutterView updates pixel format on it's layer, but the overlay views |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not magic, just missing until #48190
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just assumed it worked on main :) should have checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is very generous of you 😆
a0006c9
to
68016a7
Compare
@knopp is this ready for a review? Is there anything I can do to help get this landed in a way where we can start experimenting with it? |
It should be pretty close. I'll try to find some time tomorrow. |
b2ae8f0
to
ac76715
Compare
@jonahwilliams, this should be in reviewable state now. |
Do you have an idea about what to do for the linker failures?
|
@@ -264,7 +264,17 @@ | |||
[command_buffer waitUntilScheduled]; | |||
[drawable_ present]; | |||
} else { | |||
[command_buffer presentDrawable:drawable_]; | |||
// Check whether CAMetalLayer or FlutterMetalLayer is being used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
command_buffer presentDrawable is just a utility/helper method for what you are doing below. See https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443029-presentdrawable?language=objc
This convenience method calls the drawable’s present method after the command queue schedules the command buffer for execution. The command buffer does this by adding a completion handler by calling its own addScheduledHandler: method for you.
So I think unconditionally doing this one way would be simpler, with a note why we don't use the helper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can either link QuarzCore.framework
or check for class name directly, i.e.
if([NSStringFromClass(drawable_.layer.class) isEqualToString:@"CAMetalLayer"])
[Command_buffer present]
will also try to add some undocumented handler on the drawable, I'm not sure if it's important or not.
But I agree that we should be fine just doing this unconditionally.
- (void)addPresentedHandler:(nonnull MTLDrawablePresentedHandler)block { | ||
} | ||
|
||
- (void)presentAtTime:(CFTimeInterval)presentationTime { | ||
} | ||
|
||
- (void)presentAfterMinimumDuration:(CFTimeInterval)duration { | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For methods that aren't implemented, would it be safer to error log if they are used?
[[NSNotificationCenter defaultCenter] removeObserver:self]; | ||
} | ||
|
||
- (void)setMaxRefreshRate:(double)refreshRate forceMax:(BOOL)forceMax { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there parts of this that overlap with the other display link manager usage? Mostly curious if we have some repeated logic we need to be aware of, if so could use a note.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our display link usage is a bit of a mess. I'd like to clean it up at some point, hopefully also fixing flutter/flutter#110431 in the process.
As far as this PR is concerned, the main issue is that the iOS vsync waiter is scheduling the display link on UI thread, which does not trigger a core animation frame (CAMetalLayer
does not seem to care). So the vsync waiter displaylink is running at 120hz while core animation only syncs to compositor at 60hz. And because I don't want to touch other parts in this PR, I added a display link and copied some of the logic from vsync waiter so that it runs at same speed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I would just add a note to this part of the code with the context you provided here.
- (BOOL)isKindOfClass:(Class)aClass { | ||
#pragma clang diagnostic push | ||
#pragma clang diagnostic ignored "-Wunguarded-availability-new" | ||
// Pretend that we're a CAMetalLayer so that the rest of Flutter plays along |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there parts of flutter where this is needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied the "-Wunguarded-availability-new"
from some other part of engine. Everywhere CAMetalLayer is mentioned the simulator build will complain. Apparently it's an old well known bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:(
} else { | ||
pixelFormat = kCVPixelFormatType_32BGRA; | ||
bytesPerElement = 4; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps some kind of error log if an unsupported format was provided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if (_totalTextures < 3) { | ||
++_totalTextures; | ||
IOSurface* surface = [self createIOSurface]; | ||
MTLTextureDescriptor* textureDescriptor = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this descriptor benefit from lossy texture compresion and/or do we need to worry about setting MTLTextureUsageShaderRead regressing performance at all? We don't try to read from the framebuffer anymore, but I think with platform views that may still happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH I'm not sure how much of it actually applies for textures created from IOSurfaces. I went with something that looked like a safe default. Btw. does skia not require MTLTextureUsageShaderRead
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw. The texture that backs CAMetalLayer
drawables also has MTLTextureUsageShaderRead
set. Edit: Only when CALayer.framebufferOnly
is set to NO
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its fine to leave it as defaults.
Btw. does skia not require MTLTextureUsageShaderRead?
I'm not sure, does this correspond to the framebufferOnly setting on the CAMetalLayer?
layer.framebufferOnly = NO; |
It looks like we need it (though backdrop filters won't read from the framebuffer, i guess platform views might?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. Documentation says that with framebufferOnly
the CALayer textures will only have rendertarget usage. I'll look into it.
I don't see how platform views would read from it. They don't have access to the metal texture, or even the underlying iOSurface, only compositor does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw. we set layer.framebufferOnly
to flutter::Settings::kSurfaceDataAccessible ? NO : YES;
inside FlutterView.mm
, but then unconditionally set to NO
inside ios_surface_metal_impeller
. Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And actually seems necessary, otherwise:
[MTLDebugBlitCommandEncoder internalValidateCopyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:options:]:388: failed assertion `Copy From Texture Validation
destinationTexture must not be a framebufferOnly texture.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, so we can't even blit to the framebuffer only texture! That is good to know. If we ever get back to fixing it we'd have to look into switching to a draw to see if that works
bcf7fca
to
9450c54
Compare
This is great! it seems to work pretty well on the apps I am testing, though I admit besides an eyeball test I'm not sure the best way to measure that. One big caveat is metal system traces not working. I think with this approach, you'll need to manually start a metal system trace, but that doesn't look too difficult. https://developer.apple.com/documentation/metal/mtlcapturemanager?language=objc I also added @hellohuanlin to take a look too, but provisional LGTM 😄 . |
Metal workload capture from XCode works at least, but it must be with command queue and not frame scope. But yeah, not having CAMetalLayer will make profiling more awkward. |
We can end up submitting multiple command buffers per frame today, guessing the right about feels pretty 💩 I don't think that needs to be solved here though. What would be nice is if you could add some trivial unit tests of the flutter metal layer. The code is pretty straight forward now, but it will be helpful for future contributors if there already exists some scaffolding to add a test. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @knopp, is there any doc or issue discussion on this? Thanks
_didSetContentsDuringThisDisplayLinkPeriod = NO; | ||
// Do not pause immediately, this seems to prevent 120hz while touching. | ||
if (_displayLinkPauseCountdown == 3) { | ||
_displayLink.paused = YES; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe im missing some context - i'm having trouble understanding why are we pausing display link here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hellohuanlin for context please see https://github.com/flutter/engine/blob/a53ca8a17c85065cd69a6ce10d6f9bc7de9e899a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm .
IN short, DisplayLink ticks at a given rate and if you don't want to continually tick (because you know when you need to schedule frames) then you have to pause it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Displaylink on main thread is actually driving vsync. That includes submitting a core animation frame. I haven't really profiled the overhead from it, but very likely there is some, compared to main thread just sleeping on idle. The questionable part here is pausing after 3 frames. It might not be necessary - it's more of a better safe than sorry thing.
res = texture; | ||
} | ||
} | ||
[_availableTextures removeObject:res]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to check NPE here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What for? Neither _availableTextures
nor res
can be nil
at that place. Also invoking selectors on nil
objects is permitted in objc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh i was talking about res
- is it possible that it's not set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reading it again - it looks like res will always be set. However, it's possible that the texture is in use (texture.surface.isInUse
). Will that be fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarified this in a comment.
_didSetContentsDuringThisDisplayLinkPeriod = YES; | ||
} else if (!_displayLinkForcedMaxRate) { | ||
_displayLinkForcedMaxRate = YES; | ||
[self setMaxRefreshRate:[DisplayLinkManager displayRefreshRate] forceMax:YES]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are we changing the refresh rate here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a weird behavior on iPhone, where for several seconds after unlock, and only with variable refresh rate enabled, the main thread display link seems to fire on completely different interval than actual system refresh rate. This does not happen when non-variable refresh rate (hence forcing the refresh rate if the layer detects multiple buffers submitted during single display link interval).
It is documented after top of the class (perhaps not the best place):
// Used to track whether the content was set during this display link.
// When unlocking phone the layer (main thread) display link and raster thread
// display link get out of sync for several seconds. Even worse, layer display
// link does not seem to reflect actual vsync. Forcing the layer link
// to max rate (instead range) temporarily seems to fix the issue.
BOOL _didSetContentsDuringThisDisplayLinkPeriod;
This behavior can be observed through animation hitches instruments tool for example - after modifying vsync_waiter_ios
to fire on main thread, you will see that UI and raster threads are bing woken up at completely different period than system vsync.
It's strage that this happens and the only workaround I found was to disable variable refresh rate for a bit once this starts happening.
@hellohuanlin the context for this discussion is flutter/flutter#138490 |
im currently investigating a perf issue with admob banner (jank when scrolling 2-3 banners on screen). Do you think this chnage can be helpful? |
b01b907
to
8f6a7ff
Compare
…140280) flutter/engine@c5fc179...726a137 2023-12-16 matej.knopp@gmail.com Add FlutterMetalLayer as optional alternative to CAMetalLayer (flutter/engine#48226) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC jsimmons@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Are there any plans when this will be available in the stable channel? Since this is only activated through a flag for now, this could be more accessible for developers who want to test this feature in their production app. Switching to the master channel arises other errors such as the increased minimum Dart SDK version which is currently incompatible for a lot of packages. |
@KevinHaendel the way this works is the that it comes to the stable channel in the next release, unless it was already merged after the quarter's branch cut. The next stable will be in Q1 2024, and this change will be there. |
@knopp Huge thanks to deliver such important improvements for iOS in Flutter, you are true legend, I wish Google pay you for your expertise and patience. I don't use flutter one year already because kind of problems you just solved, but I am following your issues and PRs, true legendary. @jonahwilliams just hire this guy ffs... |
This PR implements
FlutterMetalLayer
, a drop-in (as far as Flutter is concerned) replacement forCAMetalLayer
. The biggest difference is thatFlutterMetalLayer
can present frames from background thread within aCATransaction
.FlutterMetalLayer
is disabled by default. To opt-in, add the following item toInfo.plist
:The performance seems quite good, consistent 120hz on iPhone 13 Pro.
Benefits
CALayers
, each showing different part, allowing for performant implementation of unobstructed platform views.Drawbacks