From 2b151110a158f18b4335f9173efe2f500fed840e Mon Sep 17 00:00:00 2001 From: zplata Date: Mon, 23 Oct 2023 17:52:20 +0000 Subject: [PATCH] Enable CADisplayLink to run at a user-defined preferredFramesPerSecond Just a draft for testing at the moment.. if we want to include the way to set `preferredFramesPerSecond`, might make sense to expose some kind of API on `RiveView` or `RiveViewModel` to set on `CADisplayLink` when we create it for the animation loop. In particular, the change to `Info.plist` below is what enables the display link to go to 120fps. One caveat is that in setting `.preferredFramesPerSecond`, this API is marked as deprecated by Apple.. and still need to understand what the alternative is https://developer.apple.com/documentation/quartzcore/cadisplaylink/1648421-preferredframespersecond Diffs= 97b7622bc Enable CADisplayLink to run at a user-defined preferredFramesPerSecond (#6111) Co-authored-by: Zachary Plata --- .rive_head | 2 +- .rive_renderer | 2 +- .../RiveExample.xcodeproj/project.pbxproj | 6 ++ .../Examples/SwiftUI/SwiftVariableFPS.swift | 58 +++++++++++++++++++ Example-iOS/Source/ExamplesMaster.swift | 3 +- Example-iOS/Source/Info.plist | 2 + Source/RiveView.swift | 27 ++++++++- Source/RiveViewModel.swift | 22 ++++++- 8 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 Example-iOS/Source/Examples/SwiftUI/SwiftVariableFPS.swift diff --git a/.rive_head b/.rive_head index a0b41a4a..bc0d1baa 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -d65b239c5c6ce44c3b58cd71ff17721d874a4ab1 +97b7622bcb0d99dc469706d9cf57cb03e34488de diff --git a/.rive_renderer b/.rive_renderer index 1f34f742..ae76bae1 100644 --- a/.rive_renderer +++ b/.rive_renderer @@ -1 +1 @@ -03f784cccda96fd5eee541e31bed4681904c0ed8 +e40af7f67eb7654000a156614e462c9a31f69bb9 diff --git a/Example-iOS/RiveExample.xcodeproj/project.pbxproj b/Example-iOS/RiveExample.xcodeproj/project.pbxproj index a10f98d0..aec98a5b 100644 --- a/Example-iOS/RiveExample.xcodeproj/project.pbxproj +++ b/Example-iOS/RiveExample.xcodeproj/project.pbxproj @@ -144,6 +144,8 @@ E5A7874A27E115170056F24B /* energy_bar_example.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5A7874727E115170056F24B /* energy_bar_example.riv */; }; E5A7874C27E1158E0056F24B /* prop_example.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5A7874B27E1158E0056F24B /* prop_example.riv */; }; E5CD7D7127DC331900BFE5E2 /* SwiftMeshAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CD7D7027DC331900BFE5E2 /* SwiftMeshAnimation.swift */; }; + E5E87A012AE5A83800E7295F /* SwiftVariableFPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E87A002AE5A83700E7295F /* SwiftVariableFPS.swift */; }; + E5E87A022AE5A85E00E7295F /* SwiftVariableFPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E87A002AE5A83700E7295F /* SwiftVariableFPS.swift */; }; F8772A872AD946D500AB5920 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C73E9724FC471E00EF9516 /* AppDelegate.swift */; }; F8772A882AD946FD00AB5920 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 042C88822643D6B900E7DBB2 /* Main.storyboard */; }; F8772A892AD9470000AB5920 /* ExamplesMaster.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3357CA0280F42EC00F03B6F /* ExamplesMaster.swift */; }; @@ -419,6 +421,7 @@ E5A7874727E115170056F24B /* energy_bar_example.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = energy_bar_example.riv; sourceTree = ""; }; E5A7874B27E1158E0056F24B /* prop_example.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = prop_example.riv; sourceTree = ""; }; E5CD7D7027DC331900BFE5E2 /* SwiftMeshAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMeshAnimation.swift; sourceTree = ""; }; + E5E87A002AE5A83700E7295F /* SwiftVariableFPS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftVariableFPS.swift; sourceTree = ""; }; F8772A712AD945FC00AB5920 /* Preview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Preview.app; sourceTree = BUILT_PRODUCTS_DIR; }; F8772A812AD945FE00AB5920 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F8772ADF2AD94A0500AB5920 /* Preview (macOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Preview (macOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -596,6 +599,7 @@ C9A84F342644931E0014D8E0 /* SwiftUI */ = { isa = PBXGroup; children = ( + E5E87A002AE5A83700E7295F /* SwiftVariableFPS.swift */, 04026DC327CE3ED6002B3DBF /* SwiftSimpleAnimation.swift */, C9CB2F12264C92D200E7FF0D /* SwiftWidgets.swift */, 04026DC727CE3EE6002B3DBF /* SwiftLayout.swift */, @@ -1110,6 +1114,7 @@ C3468E6227FDCBC6008652FD /* SimpleSlider.swift in Sources */, C3C074EE28414F4600E8EB33 /* SwiftTestParityAnimSM.swift in Sources */, 83C89ACB29886ECB00044C17 /* StressTest.swift in Sources */, + E5E87A012AE5A83800E7295F /* SwiftVariableFPS.swift in Sources */, C3357CA1280F42EC00F03B6F /* ExamplesMaster.swift in Sources */, C324DB5628071EB80060589F /* RiveSwitch.swift in Sources */, C3ECAC272817BE4600A81123 /* SwiftTouchEvents.swift in Sources */, @@ -1133,6 +1138,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E5E87A022AE5A85E00E7295F /* SwiftVariableFPS.swift in Sources */, F8772ACD2AD9471E00AB5920 /* SwiftMeshAnimation.swift in Sources */, F8772AC22AD9471B00AB5920 /* BlendModes.swift in Sources */, F8772AD72AD9472F00AB5920 /* SceneDelegate.swift in Sources */, diff --git a/Example-iOS/Source/Examples/SwiftUI/SwiftVariableFPS.swift b/Example-iOS/Source/Examples/SwiftUI/SwiftVariableFPS.swift new file mode 100644 index 00000000..17ff699c --- /dev/null +++ b/Example-iOS/Source/Examples/SwiftUI/SwiftVariableFPS.swift @@ -0,0 +1,58 @@ +// +// SwiftVariableFPS.swift +// Example (iOS) +// +// Created by Zach Plata on 10/20/23. +// Copyright © 2023 Rive. All rights reserved. +// + +import SwiftUI +import RiveRuntime + +struct SwiftVariableFPS: DismissableView { + var dismiss: () -> Void = {} + + private var stateChanger = RiveViewModel(fileName: "skills", stateMachineName: "Designer's Test") + + var body: some View { + ScrollView{ + VStack { + stateChanger.view() + .frame(height:200) + + HStack{ + Button("Prefer 30 fps") { + if #available(iOS 15.0, *) { + stateChanger.setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange(minimum: 30, maximum: 120, preferred: 30)) + } else { + stateChanger.setPreferredFramesPerSecond(preferredFramesPerSecond: 30) + } + } + Button("Prefer 60 fps") { + if #available(iOS 15.0, *) { + stateChanger.setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange(minimum: 30, maximum: 120, preferred: 60)) + } else { + stateChanger.setPreferredFramesPerSecond(preferredFramesPerSecond: 60) + } + } + Button("Prefer 120 fps") { + if #available(iOS 15.0, *) { + stateChanger.setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange(minimum: 30, maximum: 120, preferred: 120)) + } else { + stateChanger.setPreferredFramesPerSecond(preferredFramesPerSecond: 120) + } + } + } + + } + }.onAppear() { + if #available(iOS 15.0, *) { + stateChanger.setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange(minimum: 30, maximum: 120, preferred: 120)) + } else { + stateChanger.setPreferredFramesPerSecond(preferredFramesPerSecond: 120) + } + } + } +} + + diff --git a/Example-iOS/Source/ExamplesMaster.swift b/Example-iOS/Source/ExamplesMaster.swift index 5f3ab058..8c7fe208 100644 --- a/Example-iOS/Source/ExamplesMaster.swift +++ b/Example-iOS/Source/ExamplesMaster.swift @@ -38,7 +38,8 @@ class ExamplesMasterTableViewController: UITableViewController { ("State Machine", typeErased(dismissableView: SwiftStateMachine())), ("Mesh Animation", typeErased(dismissableView: SwiftMeshAnimation())), ("Playing with Text", typeErased(dismissableView: TextInputView())), - ("Rive Events", typeErased(dismissableView: SwiftEvents())) + ("Rive Events", typeErased(dismissableView: SwiftEvents())), + ("Variable FPS", typeErased(dismissableView: SwiftVariableFPS())) ] diff --git a/Example-iOS/Source/Info.plist b/Example-iOS/Source/Info.plist index 9be7a34a..4e9618c9 100644 --- a/Example-iOS/Source/Info.plist +++ b/Example-iOS/Source/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/Source/RiveView.swift b/Source/RiveView.swift index 3e2f135d..53cc06a3 100644 --- a/Source/RiveView.swift +++ b/Source/RiveView.swift @@ -75,6 +75,27 @@ open class RiveView: RiveRendererView { setFPSCounterVisibility() } + #if os(iOS) + /// Hints to underlying CADisplayLink the preferred FPS to run at + /// - Parameters: + /// - preferredFramesPerSecond: Integer number of seconds to set preferred FPS at + open func setPreferredFramesPerSecond(preferredFramesPerSecond: Int) { + if let displayLink = displayLinkProxy?.displayLink { + displayLink.preferredFramesPerSecond = preferredFramesPerSecond + } + } + + /// Hints to underlying CADisplayLink the preferred frame rate range + /// - Parameters: + /// - preferredFrameRateRange: Frame rate range to set + @available(iOSApplicationExtension 15.0, *) + open func setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange) { + if let displayLink = displayLinkProxy?.displayLink { + displayLink.preferredFrameRateRange = preferredFrameRateRange + } + } + #endif + // MARK: - Controls /// Starts the render loop @@ -140,8 +161,12 @@ open class RiveView: RiveRendererView { fpsCounter?.stopped() } - private func timestamp() -> Double { + private func timestamp() -> CFTimeInterval { + #if os(iOS) + return displayLinkProxy?.displayLink?.targetTimestamp ?? Date().timeIntervalSince1970 + #else return Date().timeIntervalSince1970 + #endif } diff --git a/Source/RiveViewModel.swift b/Source/RiveViewModel.swift index 1678cf63..f0489bfb 100644 --- a/Source/RiveViewModel.swift +++ b/Source/RiveViewModel.swift @@ -101,7 +101,8 @@ open class RiveViewModel: NSObject, ObservableObject, RiveFileDelegate, RiveStat fit: RiveFit = .contain, alignment: RiveAlignment = .center, autoPlay: Bool = true, - artboardName: String? = nil + artboardName: String? = nil, + preferredFramesPerSecond: Int? = nil ) { self.fit = fit self.alignment = alignment @@ -177,6 +178,25 @@ open class RiveViewModel: NSObject, ObservableObject, RiveFileDelegate, RiveStat didSet { riveView?.alignment = alignment } } + #if os(iOS) + /// Hints to underlying CADisplayLink in RiveView (if created) the preferred FPS to run at + /// For more, see: https://developer.apple.com/documentation/quartzcore/cadisplaylink/1648421-preferredframespersecond + /// - Parameters: + /// - preferredFramesPerSecond: Integer number of seconds to set preferred FPS at + public func setPreferredFramesPerSecond(preferredFramesPerSecond: Int) { + riveView?.setPreferredFramesPerSecond(preferredFramesPerSecond: preferredFramesPerSecond) + } + + /// Hints to underlying CADisplayLink in RiveView (if created) the preferred frame rate range + /// For more, see: https://developer.apple.com/documentation/quartzcore/cadisplaylink/3875343-preferredframeraterange + /// - Parameters: + /// - preferredFrameRateRange: Frame rate range to set + @available(iOSApplicationExtension 15.0, *) + public func setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange) { + riveView?.setPreferredFrameRateRange(preferredFrameRateRange: preferredFrameRateRange) + } + #endif + /// Starts the active Animation or StateMachine from it's last position. It will start /// from the beginning if the active Animation has ended or a new one is provided. /// - Parameters: