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

[ios] Lock refresh rate to 80fps when threads are merged #39172

Merged
merged 1 commit into from
Feb 3, 2023
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
78 changes: 66 additions & 12 deletions shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#include "flutter/fml/raster_thread_merger.h"
#include "flutter/fml/thread.h"

#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
Expand Down Expand Up @@ -65,11 +66,11 @@ - (void)testSetCorrectVariableRefreshRates {
callback:callback] autorelease];
CADisplayLink* link = [vsyncClient getDisplayLink];
if (@available(iOS 15.0, *)) {
XCTAssertEqual(link.preferredFrameRateRange.maximum, maxFrameRate);
XCTAssertEqual(link.preferredFrameRateRange.preferred, maxFrameRate);
XCTAssertEqual(link.preferredFrameRateRange.minimum, maxFrameRate / 2);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, maxFrameRate, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, maxFrameRate, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, maxFrameRate / 2, 0.1);
} else {
XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate);
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, maxFrameRate, 0.1);
}
[vsyncClient release];
}
Expand All @@ -88,11 +89,11 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs
callback:callback] autorelease];
CADisplayLink* link = [vsyncClient getDisplayLink];
if (@available(iOS 15.0, *)) {
XCTAssertEqual(link.preferredFrameRateRange.maximum, 0);
XCTAssertEqual(link.preferredFrameRateRange.preferred, 0);
XCTAssertEqual(link.preferredFrameRateRange.minimum, 0);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, 0, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, 0, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, 0, 0.1);
} else {
XCTAssertEqual(link.preferredFramesPerSecond, 0);
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, 0, 0.1);
}
[vsyncClient release];
}
Expand All @@ -107,11 +108,11 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs
callback:callback] autorelease];
CADisplayLink* link = [vsyncClient getDisplayLink];
if (@available(iOS 15.0, *)) {
XCTAssertEqual(link.preferredFrameRateRange.maximum, 0);
XCTAssertEqual(link.preferredFrameRateRange.preferred, 0);
XCTAssertEqual(link.preferredFrameRateRange.minimum, 0);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, 0, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, 0, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, 0, 0.1);
} else {
XCTAssertEqual(link.preferredFramesPerSecond, 0);
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, 0, 0.1);
}
[vsyncClient release];
}
Expand All @@ -135,4 +136,57 @@ - (void)testAwaitAndPauseWillWorkCorrectly {
[vsyncClient release];
}

- (void)testRefreshRateUpdatedTo80WhenThraedsMerge {
auto platform_thread_task_runner = CreateNewThread("Platform");
auto raster_thread_task_runner = CreateNewThread("Raster");
auto ui_thread_task_runner = CreateNewThread("UI");
auto io_thread_task_runner = CreateNewThread("IO");
auto task_runners =
flutter::TaskRunners("test", platform_thread_task_runner, raster_thread_task_runner,
ui_thread_task_runner, io_thread_task_runner);

id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
[[[mockDisplayLinkManager stub] andReturnValue:@(YES)] maxRefreshRateEnabledOnIPhone];
auto vsync_waiter = flutter::VsyncWaiterIOS(task_runners);

fml::scoped_nsobject<VSyncClient> vsyncClient = vsync_waiter.GetVsyncClient();
CADisplayLink* link = [vsyncClient.get() getDisplayLink];

if (@available(iOS 15.0, *)) {
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, maxFrameRate, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, maxFrameRate, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, maxFrameRate / 2, 0.1);
} else {
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, maxFrameRate, 0.1);
}

const auto merger = fml::RasterThreadMerger::CreateOrShareThreadMerger(
nullptr, platform_thread_task_runner->GetTaskQueueId(),
raster_thread_task_runner->GetTaskQueueId());

merger->MergeWithLease(5);
vsync_waiter.AwaitVSync();

if (@available(iOS 15.0, *)) {
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, 80, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, 80, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, 60, 0.1);
} else {
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, 80, 0.1);
}

merger->UnMergeNowIfLastOne();
vsync_waiter.AwaitVSync();

if (@available(iOS 15.0, *)) {
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.maximum, maxFrameRate, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.preferred, maxFrameRate, 0.1);
XCTAssertEqualWithAccuracy(link.preferredFrameRateRange.minimum, maxFrameRate / 2, 0.1);
} else {
XCTAssertEqualWithAccuracy(link.preferredFramesPerSecond, maxFrameRate, 0.1);
}
}

@end
18 changes: 16 additions & 2 deletions shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@

@interface DisplayLinkManager : NSObject

// Whether the max refresh rate on iPhone Pro-motion devices are enabled.
Copy link
Member

Choose a reason for hiding this comment

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

Pro-motion iPad Pros too, right?

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 is only for iPhone, iPad always has high refresh rate on, let me add a comment about it too.

// This reflects the value of `CADisableMinimumFrameDurationOnPhone` in the
// info.plist file.
//
// Note on iPads that support Pro-motion, the max refresh rate is always enabled.
@property(class, nonatomic, readonly) BOOL maxRefreshRateEnabledOnIPhone;

//------------------------------------------------------------------------------
/// @brief The display refresh rate used for reporting purposes. The engine does not care
/// about this for frame scheduling. It is only used by tools for instrumentation. The
Expand Down Expand Up @@ -51,6 +58,8 @@

- (double)getRefreshRate;

- (void)setMaxRefreshRate:(double)refreshRate;

@end

namespace flutter {
Expand All @@ -64,12 +73,17 @@ class VsyncWaiterIOS final : public VsyncWaiter, public VariableRefreshRateRepor
// |VariableRefreshRateReporter|
double GetRefreshRate() const override;

private:
fml::scoped_nsobject<VSyncClient> client_;
// Made public for testing.
fml::scoped_nsobject<VSyncClient> GetVsyncClient() const;

// |VsyncWaiter|
// Made public for testing.
void AwaitVSync() override;

private:
fml::scoped_nsobject<VSyncClient> client_;
double max_refresh_rate_;

FML_DISALLOW_COPY_AND_ASSIGN(VsyncWaiterIOS);
};

Expand Down
38 changes: 31 additions & 7 deletions shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@

#include "flutter/common/task_runners.h"
#include "flutter/fml/logging.h"
#include "flutter/fml/memory/task_runner_checker.h"
#include "flutter/fml/trace_event.h"

// When calculating refresh rate diffrence, anything within 0.1 fps is ignored.
const static double kRefreshRateDiffToIgnore = 0.1;

namespace flutter {

VsyncWaiterIOS::VsyncWaiterIOS(const flutter::TaskRunners& task_runners)
Expand All @@ -26,6 +30,7 @@
client_ =
fml::scoped_nsobject{[[VSyncClient alloc] initWithTaskRunner:task_runners_.GetUITaskRunner()
callback:callback]};
max_refresh_rate_ = [DisplayLinkManager displayRefreshRate];
}

VsyncWaiterIOS::~VsyncWaiterIOS() {
Expand All @@ -35,6 +40,19 @@
}

void VsyncWaiterIOS::AwaitVSync() {
double new_max_refresh_rate = [DisplayLinkManager displayRefreshRate];
if (fml::TaskRunnerChecker::RunsOnTheSameThread(
task_runners_.GetRasterTaskRunner()->GetTaskQueueId(),
task_runners_.GetPlatformTaskRunner()->GetTaskQueueId())) {
// Pressure tested on iPhone 13 pro, the oldest iPhone that supports refresh rate greater than
// 60fps. A flutter app can handle fast scrolling on 80 fps with 6 PlatformViews in the scene at
// the same time.
new_max_refresh_rate = 80;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I originally set it to 60, but it looks like we can def hit 80 consistently. I decided to lock it at 80 so the scrolling with PlatformView is still smoother than 60fps.

This might change when we re-evaluate what the default fps we want the Flutter apps to run on after we implement a new API for the APP to set refresh rates, see flutter/flutter#119268

Copy link
Member

Choose a reason for hiding this comment

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

Can you add a comment for why you chose this? What hardware did you try?

}
if (fabs(new_max_refresh_rate - max_refresh_rate_) > kRefreshRateDiffToIgnore) {
max_refresh_rate_ = new_max_refresh_rate;
[client_.get() setMaxRefreshRate:max_refresh_rate_];
}
[client_.get() await];
}

Expand All @@ -43,6 +61,10 @@
return [client_.get() getRefreshRate];
}

fml::scoped_nsobject<VSyncClient> VsyncWaiterIOS::GetVsyncClient() const {
return client_;
}

} // namespace flutter

@implementation VSyncClient {
Expand All @@ -64,7 +86,7 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
};
display_link_.get().paused = YES;

[self setMaxRefreshRateIfEnabled];
[self setMaxRefreshRate:[DisplayLinkManager displayRefreshRate]];

task_runner->PostTask([client = [self retain]]() {
[client->display_link_.get() addToRunLoop:[NSRunLoop currentRunLoop]
Expand All @@ -76,15 +98,12 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
return self;
}

- (void)setMaxRefreshRateIfEnabled {
NSNumber* minimumFrameRateDisabled =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"];
if (![minimumFrameRateDisabled boolValue]) {
- (void)setMaxRefreshRate:(double)refreshRate {
if (!DisplayLinkManager.maxRefreshRateEnabledOnIPhone) {
return;
}
double maxFrameRate = fmax([DisplayLinkManager displayRefreshRate], 60);
double maxFrameRate = fmax(refreshRate, 60);
double minFrameRate = fmax(maxFrameRate / 2, 60);

if (@available(iOS 15.0, *)) {
display_link_.get().preferredFrameRateRange =
CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate);
Expand Down Expand Up @@ -170,4 +189,9 @@ - (void)onDisplayLink:(CADisplayLink*)link {
// no-op.
}

+ (BOOL)maxRefreshRateEnabledOnIPhone {
return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]
boolValue];
}

@end