diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index fcd6de7e2b18b..c17340afa5ac2 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1546,6 +1546,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Int FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index a9dfa50c132c7..0a46c602f94c3 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -208,6 +208,7 @@ source_set("ios_test_flutter_mrc") { "framework/Source/FlutterPlatformPluginTest.mm", "framework/Source/FlutterPlatformViewsTest.mm", "framework/Source/FlutterViewTest.mm", + "framework/Source/VsyncWaiterIosTest.mm", "framework/Source/accessibility_bridge_test.mm", "platform_message_handler_ios_test.mm", ] diff --git a/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm b/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm new file mode 100644 index 0000000000000..e264644c2bd25 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#include "flutter/fml/thread.h" + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" + +FLUTTER_ASSERT_NOT_ARC +namespace { +fml::RefPtr CreateNewThread(std::string name) { + auto thread = std::make_unique(name); + auto runner = thread->GetTaskRunner(); + return runner; +} +} + +@interface VSyncClient (Testing) + +- (CADisplayLink*)getDisplayLink; + +@end + +@interface VsyncWaiterIosTest : XCTestCase +@end + +@implementation VsyncWaiterIosTest + +- (void)testSetCorrectVariableRefreshRates { + auto thread_task_runner = CreateNewThread("VsyncWaiterIosTest"); + auto callback = [](std::unique_ptr recorder) {}; + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]) + .andReturn(@YES); + id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; + double maxFrameRate = 120; + [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; + + VSyncClient* vsyncClient = [[[VSyncClient alloc] initWithTaskRunner:thread_task_runner + 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); + } else if (@available(iOS 10.0, *)) { + XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate); + } + [vsyncClient release]; +} + +- (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIsNotOn { + auto thread_task_runner = CreateNewThread("VsyncWaiterIosTest"); + auto callback = [](std::unique_ptr recorder) {}; + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]) + .andReturn(@NO); + id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; + double maxFrameRate = 120; + [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; + + VSyncClient* vsyncClient = [[[VSyncClient alloc] initWithTaskRunner:thread_task_runner + 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); + } else if (@available(iOS 10.0, *)) { + XCTAssertEqual(link.preferredFramesPerSecond, 0); + } + [vsyncClient release]; +} + +- (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIsNotSet { + auto thread_task_runner = CreateNewThread("VsyncWaiterIosTest"); + auto callback = [](std::unique_ptr recorder) {}; + id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; + double maxFrameRate = 120; + [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; + VSyncClient* vsyncClient = [[[VSyncClient alloc] initWithTaskRunner:thread_task_runner + 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); + } else if (@available(iOS 10.0, *)) { + XCTAssertEqual(link.preferredFramesPerSecond, 0); + } + [vsyncClient release]; +} + +@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 f5c3e656b258c..9b5d5aa519bd9 100644 --- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h +++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h @@ -5,6 +5,8 @@ #ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_VSYNC_WAITER_IOS_H_ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_VSYNC_WAITER_IOS_H_ +#include + #include "flutter/fml/macros.h" #include "flutter/fml/memory/weak_ptr.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" 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 91cabc13dd985..e635872010815 100644 --- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm +++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -64,6 +63,8 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr)task_runner }; display_link_.get().paused = YES; + [self setMaxRefreshRateIfEnabled]; + task_runner->PostTask([client = [self retain]]() { [client->display_link_.get() addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; @@ -74,6 +75,23 @@ - (instancetype)initWithTaskRunner:(fml::RefPtr)task_runner return self; } +- (void)setMaxRefreshRateIfEnabled { + NSNumber* minimumFrameRateDisabled = + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]; + if (![minimumFrameRateDisabled boolValue]) { + return; + } + double maxFrameRate = fmax([DisplayLinkManager displayRefreshRate], 60); + double minFrameRate = fmax(maxFrameRate / 2, 60); + + if (@available(iOS 15.0, *)) { + display_link_.get().preferredFrameRateRange = + CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate); + } else if (@available(iOS 10.0, *)) { + display_link_.get().preferredFramesPerSecond = maxFrameRate; + } +} + - (void)await { display_link_.get().paused = NO; } @@ -99,7 +117,6 @@ - (void)onDisplayLink:(CADisplayLink*)link { recorder->RecordVsync(frame_start_time, frame_target_time); display_link_.get().paused = YES; - callback_(std::move(recorder)); } @@ -117,6 +134,10 @@ - (double)getRefreshRate { return current_refresh_rate_; } +- (CADisplayLink*)getDisplayLink { + return display_link_.get(); +} + @end @implementation DisplayLinkManager diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index e429e7ca47784..fc3929cfcf48f 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 0D6AB6C922BB05E200EEE540 /* IosUnitTestsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IosUnitTestsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6CF22BB05E200EEE540 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FlutterEngineConfig.xcconfig; sourceTree = ""; }; + 68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = ""; }; F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = ""; }; F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libocmock_shared.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libocmock_shared.dylib"; sourceTree = ""; }; F77E081726FA9CE6003E6E4C /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = "../../../../out/$(FLUTTER_ENGINE)/Flutter.framework"; sourceTree = ""; }; @@ -96,6 +97,7 @@ 0AC232E924BA71D300A85907 /* Source */ = { isa = PBXGroup; children = ( + 68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */, F7A3FDE026B9E0A300EADD61 /* FlutterAppDelegateTest.mm */, 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */, 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */,