Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scrolling is not true 120hz and feels laggy on ProMotion iPhone 13 #90675

Closed
acoutts opened this issue Sep 24, 2021 · 49 comments · Fixed by flutter/engine#29797
Closed

Scrolling is not true 120hz and feels laggy on ProMotion iPhone 13 #90675

acoutts opened this issue Sep 24, 2021 · 49 comments · Fixed by flutter/engine#29797
Assignees
Labels
c: performance Relates to speed or footprint issues (see "perf:" labels) customer: alibaba customer: crowd Affects or could affect many people, though not necessarily a specific customer. customer: product e: device-specific Only manifests on certain devices engine flutter/engine repository. See also e: labels. P2 Important issues not at the top of the work list platform-ios iOS applications specifically r: fixed Issue is closed as already fixed in a newer version

Comments

@acoutts
Copy link

acoutts commented Sep 24, 2021

Even though the performance overlay says the frame timing should easily produce 120fps, it feels different than native iOS apps on the iPhone 13- a bit slower and janky. this is not sksl jank

I took some slow motion videos to demonstrate the scrolling performance issue. When you play it back, scrub frame by frame and you will see there is a lot more jank between screen updates on the flutter app vs the native messages one.

You'll see the performance profiler reporting 2.7ms/frame which in theory should mean it's capable of 370fps+ but as you can see there is a big difference between the frames on the native messages app and the flutter sample app.

Flutter app video (iPhone 13 Pro):
https://drive.google.com/file/d/1Qwqty1dCoGUyIfB5t5WeBf1s6czQhzFZ/view

Native iOS messages app (iPhone 13 Pro):
https://drive.google.com/file/d/1w5cw_PaIiMPTlyLuq2dKo86-KsWr-FEN/view

In case Google Drive applies some processing to the videos, here's a zip file with them untouched:
https://drive.google.com/file/d/1kqDEEI50Bs1uctgePvHKM-hJEE0QUhrA/view

Steps to Reproduce

Example app available here:
https://github.com/acoutts/flutter_120hz_bug

Expected results:
Flutter apps should run as smooth as native apps.

Actual results:
Flutter performance does not match the native experience on 120hz devices.

Logs
[✓] Flutter (Channel stable, 2.5.1, on macOS 11.6 20G165 darwin-x64, locale en-US)
    • Flutter version 2.5.1 at /Users/andrewcoutts/Projects/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ffb2ecea52 (7 days ago), 2021-09-17 15:26:33 -0400
    • Engine revision b3af521a05
    • Dart version 2.14.2

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/andrewcoutts/Library/Android/sdk
    • Platform android-30, build-tools 30.0.2
    • ANDROID_HOME = /Users/andrewcoutts/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 13.0, Build version 13A233
    • CocoaPods version 1.11.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6222593)

[✓] VS Code (version 1.52.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.12.1

[✓] VS Code (version 1.61.0-insider)
    • VS Code at /Applications/Visual Studio Code - Insiders.app/Contents
    • Flutter extension version 3.26.0

[✓] Connected device (4 available)
    • macOS (desktop)     • macos                     • darwin-x64     • macOS 11.6 20G165 darwin-x64
    • Chrome (web)        • chrome                    • web-javascript • Google Chrome 94.0.4606.61

• No issues found!
@acoutts
Copy link
Author

acoutts commented Sep 24, 2021

This looks potentially related: https://twitter.com/ChristianSelig/status/1441419862417494018

UIView.animateWithDuration APIs aren't clocked at 120Hz on iPhone 13? On UIScrollView, system ones, and Metal by the looks of it, rest is still 60Hz

@sroddy
Copy link
Contributor

sroddy commented Sep 25, 2021

Flutter uses Metal, so it should be already capable of reaching 120hz. I think that the current issue is that the vsync waiter currently checks for the reported frame rate from the CADisplayLink that is still 60.
https://github.com/flutter/engine/blob/master/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm#L103

The reason why this happens could be related to this:
https://www.macrumors.com/2021/09/24/apple-promotion-third-party-app-support/

I think it should be fairly simple to support the right refresh rate once it’s properly reported by the iOS SDK.

On the other hand I’m worried that supporting a completely adaptive scenario (dynamically switching between 10 and 120hz), like the one that will be natively supported in the upcoming future also by third-party apps, could be way less trivial to achieve

@acoutts
Copy link
Author

acoutts commented Sep 25, 2021

Thanks @sroddy

I just tried adding CADisableMinimumFrameDurationOnPhone to my Info.plist but it had no noticeable impact.

https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro

@CodeGlitch
Copy link

According to apple:

The answer came from Apple today. The company told AppleInsider that the 120Hz ProMotion screen will work on all third-party apps although developers need to add a plist entry in order for their apps to work with the faster refresh rate.
https://www.phonearena.com/news/apple-says-120hz-refresh-rate-works-with-all-apps_id135301

@sroddy
Copy link
Contributor

sroddy commented Sep 25, 2021

@knopp
Copy link
Member

knopp commented Sep 26, 2021

It's not just the plist entry, it is also necessary to set preferredFrameRateRange on CADisplayLink. The range should ideally depend on what the application is doing, not just set to 120fps.

@TahaTesser TahaTesser added the in triage Presently being triaged by the triage team label Sep 27, 2021
@TahaTesser TahaTesser added e: device-specific Only manifests on certain devices engine flutter/engine repository. See also e: labels. passed first triage platform-ios iOS applications specifically c: performance Relates to speed or footprint issues (see "perf:" labels) customer: crowd Affects or could affect many people, though not necessarily a specific customer. and removed in triage Presently being triaged by the triage team labels Sep 27, 2021
@Kounex
Copy link

Kounex commented Sep 27, 2021

For the time being, I showcased a sample implementation of how to enable the ProMotion feature: https://twitter.com/Kounexx/status/1442529256706396164

You can do this until there is an official implementation!

@acoutts
Copy link
Author

acoutts commented Sep 27, 2021

Can confirm, this does allow the app to run at 120hz. Cheers @Kounex

AppDelegate.swift

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {        
        if #available(iOS 15.0, *) {
            let displayLink = CADisplayLink(target: self, selector: #selector(step))
            displayLink.preferredFrameRateRange = CAFrameRateRange(minimum:80, maximum:120, preferred:120)
            displayLink.add(to: .current, forMode: .default)
                                                
        }
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    @objc func step(displaylink: CADisplayLink) {
        // Will be called once a frame has been built while matching desired frame rate
    }

But I guess that also means the app always runs at 120hz, which could be pretty bad on battery right? Do we need to wrap this into a method call and remember to turn the framerate up/down as we need it, or will the flutter engine do this for us in the future?

edit
Now i'm not sure if this actually changes it, because setting min/max/preferred to 10 doesn't change it like you would expect.

@chinmaygarde chinmaygarde added the P2 Important issues not at the top of the work list label Sep 27, 2021
@chinmaygarde
Copy link
Member

cc @jmagman

@jmagman
Copy link
Member

jmagman commented Sep 27, 2021

/cc @iskakaushik this may be of interest to you. A good excuse to pick up a iPhone 13 Pro test device.

@Kounex
Copy link

Kounex commented Sep 27, 2021

The behaviour seems to be strange indeed. Looking at the documentation, it even suggests variations like this:

CAFrameRateRange(minimum:8, maximum:15, preferred:0)

Ultimately the best solution will probably be that Flutter applies different frame rates for different use cases (list scroll, expanding, progress bar etc.) instead of applying it in general. Testing the battery performance would be interesting as well!

@acoutts
Copy link
Author

acoutts commented Sep 27, 2021

I think this needs to set in the engine, as per @sroddy:

Flutter uses Metal, so it should be already capable of reaching 120hz. I think that the current issue is that the vsync waiter currently checks for the reported frame rate from the CADisplayLink that is still 60.
https://github.com/flutter/engine/blob/master/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm#L103

Bigger issue might be we need an api exposed to change it at runtime.

@jmagman
Copy link
Member

jmagman commented Sep 27, 2021

@chinmaygarde closed a similar issue a few years ago for the iPad Pro. From #27259 (comment)

We already support 120Hz on devices with ProMotion displays. We get this out of the box because we don't set a -[CADisplayLink preferredFramesPerSecond] on the vsync waiter implementation on iOS. If there is no preference, iOS sets this to the maximum supported display refresh rate. To verify, I captured this trace on an iPad with a ProMotion display. As marked, the frame interval is ~8.3ms (1/120).

Screen Shot 2019-08-14 at 12 51 50 PM

There's an open issue to add an iPad Pro to the devicelab for benchmarking. #31450.

preferredFramesPerSecond is deprecated, maybe the problem is that this check should instead use preferredFrameRateRange.preferred on iOS 15+? I would hope they would just be aliasing one to the other though.
https://github.com/flutter/engine/blob/adfea3cf1f6a7ac581a7717ef497e32a5bbb29f5/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm#L110

And flutter create should maybe set CADisableMinimumFrameDurationOnPhone to the Info.plist to support the iPhone 13 Pro? Not sure if this is necessary if the engine isn't setting preferredFrameRateRange anywhere:

Your app must use this key to access higher frame rates (above 60Hz) it sets in the preferredFrameRateRange hint API. The iPad Pro doesn’t require this special configuration.

@sroddy
Copy link
Contributor

sroddy commented Sep 27, 2021

I think that the approach you are testing “kind-of” works because the screen refreshes at the max frame rate declared by any of the currently active animations.

if you add a CADisplayLink on the current run loop that refreshes at 120fps, all the CADisplayLink, including the one registered by the Flutter Engine, will start having their callback invoked at 120fps (unless they have a min value specified-which the one used by Flutter Engine is not having)

I think that there is no way to slow everything down below 60fps because the Flutter’s one is currently using the default settings and is set to run at 60fps min by default.

@acoutts
Copy link
Author

acoutts commented Sep 27, 2021

@jmagman could we possibly bump the priority above p4? All native iOS apps support 120hz out of the box for things like scrolling, and it's only a potential issue when they have some kind of custom animation to show.

For flutter apps, everything including scrolling is being rendered at 60hz which makes flutter apps feel laggy by comparison and feels like a big regression in app performance for anybody who simply upgrades their phone. This affects everybody with a flutter app deployed in production right now.

@acoutts
Copy link
Author

acoutts commented Sep 27, 2021

@sroddy it only sets to 60fps if it's not at least iOS 10.3. I used some print statements to see what a default CADisplayLink reports without setting preferredFrameRateRange.

Screen Shot 2021-09-27 at 6 19 54 PM

The output:

2021-09-27 18:18:33.290238-0400 Runner[1979:597023] maximumFramesPerSecond before | 120
2021-09-27 18:18:33.292048-0400 Runner[1979:597023] display link preferredFrameRateRange before | CAFrameRateRange(minimum: 0.0, maximum: 0.0, __preferred: 0.0)
2021-09-27 18:18:33.292097-0400 Runner[1979:597023] display link preferredFrameRateRange after | CAFrameRateRange(minimum: 80.0, maximum: 120.0, __preferred: 120.0)
2021-09-27 18:18:33.294646-0400 Runner[1979:597023] maximumFramesPerSecond after | 120

Looking at how the flutter engine sets up the CADisplayLink,

Screen Shot 2021-09-27 at 6 20 40 PM

It appears that a CADisplayLink by default returns 0 for everything, which means it would simply return maximumFramesPerSecond, which on my iPhone 13 Pro reports as 120 from the log output above.

This means flutter is already receiving the display refresh rate as 120.

Note: the app is still rendering everything at 60hz even with this in place (with the appropriate Info.plist entry too).

edit

Another test, if I set CADisableMinimumFrameDurationOnPhone false then maximumFramesPerSecond is still reported as 120 so it seems this plist entry has no effect for how Flutter computes displayRefreshRate, if that is what our issue is.

@sroddy
Copy link
Contributor

sroddy commented Sep 27, 2021

@jmagman @acoutts

Depending on how the MetalKit view has been implemented inside the Engine (I didn't have the time to investigate) it may also be needed to play with the preferredFramesPerSecond property of the MTKView itself.

This WWCD session is very informative about how to potentially deal with this "madness"
https://developer.apple.com/videos/play/wwdc2021/10147

But in order to really benefit from the ProMotion display features, I think devs would need a way to specify the desired frame rate of a given animation, so that the frames can be provided at the desired pace and the ProMotion display would automatically adapt its frame rate to optimise rendering and battery, given the various conditions.

@brucevang
Copy link

brucevang commented Sep 28, 2021

It's still showing 60hz after implementing the changes above. Testing a list view on my iPhone 13 Pro Max and it's still laggy compared to native apps and the App Store. You can definitely tell the difference.

@alboiuvlad29
Copy link

alboiuvlad29 commented Sep 29, 2021

Not sure wether this helps, but there seems to be a strange behaviour that could potentially be from Apple's side.

This is the content of AppDelegate.swift used in our app:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  var displayLink : CADisplayLink?
    
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    
    let controller = self.window.rootViewController as! FlutterViewController
    
    displayLink = CADisplayLink(target: self, selector: #selector(displayLinkCallback))
    displayLink!.add(to: .current, forMode: .default)
      if #available(iOS 15.0, *) {
          displayLink!.preferredFrameRateRange = CAFrameRateRange(minimum:120, maximum:120, preferred:120)
      } else {
          // Fallback on earlier versions
      }
      
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    
    @objc func displayLinkCallback() {
         // let workingTime = displayLink!.targetTimestamp - CACurrentMediaTime()
        // At this moment in time, there are `workingTime` seconds available to
        // generate content for the next frame, so
        // Core Animation can render it to the display.
    }

}

with <key>CADisableMinimumFrameDurationOnPhone</key><true/>.

Starting the app for the first time on an iPhone 13 Pro Max, the app runs at 60Hz.

Going to control centre and turning on Low power mode, then off will force the app to run in 120hz mode until the app is force closed and started again. (Scrolling is as smooth as native, and transitions as well, etc).

I tried the same behaviour with CADisableMinimumFrameDurationOnPhone set to false, but it doesn't work and the app is locked at 60Hz.

@isyshuai
Copy link

Is there any alternative to modifying the engine
2.8.0 still has this problem, is 2.8.1 fixed?

@acoutts
Copy link
Author

acoutts commented Jan 21, 2022

Excited to see #96710 and flutter/engine#30900!

@jgrandchavin

This comment was marked as duplicate.

@prilepskiy

This comment was marked as duplicate.

@hetao29

This comment was marked as duplicate.

@jmagman
Copy link
Member

jmagman commented Feb 7, 2022

This will be done with flutter/engine#29797 as described in https://flutter.dev/go/variable-refresh-rate which is blocked by #85555 which is blocked by #94078 which should be done in the next few weeks.

Please refrain from commenting "me too"s on this issue, just give the issue a 👍 and you'll get updates. We know it's a problem and we're working on it. 🙂

@jmagman
Copy link
Member

jmagman commented Mar 29, 2022

The iOS 15 SDK is now available in the engine #85555.

@cyanglaz
Copy link
Contributor

cyanglaz commented Apr 4, 2022

The engine fixed has been rolled into flutter/master.
If you would like to test this feature, please point your flutter to the master channel:

$ flutter channel master
$ flutter upgrade

To enable 120hz. Also make sure you have <key>CADisableMinimumFrameDurationOnPhone</key><true/> in the <path_to_your_app>/ios/Runner/Info.plist

@jmagman
Copy link
Member

jmagman commented Apr 4, 2022

We're particularly interested in:

  1. Does it resolve the laggy feel?
  2. How does this impact battery life? Stress-testing your app with this change would be very helpful.
  3. You shouldn't have to manually update Info.plist, did the flutter tool add the key for you?

Please let us know if you have any feedback!

@eric8810
Copy link

eric8810 commented Apr 8, 2022

@jmagman

  1. Does it resolve the laggy feel?
    laggy feel of scroll and animations are gone, but its still feels laggy when I use gesture navigation between routes.

  2. How does this impact battery life? Stress-testing your app with this change would be very helpful.
    untest

  3. You shouldn't have to manually update Info.plist, did the flutter tool add the key for you?
    info.plist was updated with flutter create

@jmagman
Copy link
Member

jmagman commented Apr 8, 2022

Thanks @eric8810

still feels laggy when I use gesture navigation between routes

Can you file a new issue for this? Likely unrelated to the app refresh rate.

@felixwortmann
Copy link

felixwortmann commented Apr 8, 2022

Does it resolve the laggy feel?

  • It mainly does. But if I scroll very slowly, it still does not seem to adapt perfectly so there is still some jittering. This is not visible (for me) when scrolling at a "normal use" speed.

How does this impact battery life?

  • I did not test this yet

You shouldn't have to manually update Info.plist, did the flutter tool add the key for you?

  • Key was added using flutter create

@eric8810
Copy link

eric8810 commented Apr 10, 2022

Thanks @eric8810

still feels laggy when I use gesture navigation between routes

Can you file a new issue for this? Likely unrelated to the app refresh rate.

see #101653

@github-actions
Copy link

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 24, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
c: performance Relates to speed or footprint issues (see "perf:" labels) customer: alibaba customer: crowd Affects or could affect many people, though not necessarily a specific customer. customer: product e: device-specific Only manifests on certain devices engine flutter/engine repository. See also e: labels. P2 Important issues not at the top of the work list platform-ios iOS applications specifically r: fixed Issue is closed as already fixed in a newer version
Projects
None yet
Development

Successfully merging a pull request may close this issue.