diff --git a/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm b/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm index 15d3f6df652ba..dda84f9ac158e 100644 --- a/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm +++ b/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm @@ -5,6 +5,7 @@ #import #import +#include "flutter/fml/raster_thread_merger.h" #include "flutter/fml/thread.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" @@ -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]; } @@ -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]; } @@ -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]; } @@ -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 = 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 diff --git a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h index 14beef5401a9f..36a608db7deca 100644 --- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h +++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h @@ -15,6 +15,13 @@ @interface DisplayLinkManager : NSObject +// Whether the max refresh rate on iPhone Pro-motion devices are enabled. +// 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 @@ -51,6 +58,8 @@ - (double)getRefreshRate; +- (void)setMaxRefreshRate:(double)refreshRate; + @end namespace flutter { @@ -64,12 +73,17 @@ class VsyncWaiterIOS final : public VsyncWaiter, public VariableRefreshRateRepor // |VariableRefreshRateReporter| double GetRefreshRate() const override; - private: - fml::scoped_nsobject client_; + // Made public for testing. + fml::scoped_nsobject GetVsyncClient() const; // |VsyncWaiter| + // Made public for testing. void AwaitVSync() override; + private: + fml::scoped_nsobject client_; + double max_refresh_rate_; + FML_DISALLOW_COPY_AND_ASSIGN(VsyncWaiterIOS); }; diff --git a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm index 207205f7288d8..5ed238704cc74 100644 --- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm +++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm @@ -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) @@ -26,6 +30,7 @@ client_ = fml::scoped_nsobject{[[VSyncClient alloc] initWithTaskRunner:task_runners_.GetUITaskRunner() callback:callback]}; + max_refresh_rate_ = [DisplayLinkManager displayRefreshRate]; } VsyncWaiterIOS::~VsyncWaiterIOS() { @@ -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; + } + 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]; } @@ -43,6 +61,10 @@ return [client_.get() getRefreshRate]; } +fml::scoped_nsobject VsyncWaiterIOS::GetVsyncClient() const { + return client_; +} + } // namespace flutter @implementation VSyncClient { @@ -64,7 +86,7 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr)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] @@ -76,15 +98,12 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr)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); @@ -170,4 +189,9 @@ - (void)onDisplayLink:(CADisplayLink*)link { // no-op. } ++ (BOOL)maxRefreshRateEnabledOnIPhone { + return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"] + boolValue]; +} + @end