Swiftgram/submodules/Display/Source/DisplayLinkAnimator.swift
2023-12-21 18:13:54 +04:00

388 lines
12 KiB
Swift

import Foundation
import UIKit
import Darwin
public protocol SharedDisplayLinkDriverLink: AnyObject {
var isPaused: Bool { get set }
func invalidate()
}
private let isIpad: Bool = {
var systemInfo = utsname()
uname(&systemInfo)
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
ptr in String.init(validatingUTF8: ptr)
}
}
if let modelCode {
if modelCode.lowercased().hasPrefix("ipad") {
return true
}
}
return false
}()
public final class SharedDisplayLinkDriver {
public enum FramesPerSecond: Comparable {
case fps(Int)
case max
public static func <(lhs: FramesPerSecond, rhs: FramesPerSecond) -> Bool {
switch lhs {
case let .fps(lhsFps):
switch rhs {
case let .fps(rhsFps):
return lhsFps < rhsFps
case .max:
return true
}
case .max:
return false
}
}
}
public typealias Link = SharedDisplayLinkDriverLink
public static let shared = SharedDisplayLinkDriver()
public final class LinkImpl: Link {
private let driver: SharedDisplayLinkDriver
public let framesPerSecond: FramesPerSecond
let update: (CGFloat) -> Void
var isValid: Bool = true
public var isPaused: Bool = false {
didSet {
if self.isPaused != oldValue {
self.driver.requestUpdate()
}
}
}
init(driver: SharedDisplayLinkDriver, framesPerSecond: FramesPerSecond, update: @escaping (CGFloat) -> Void) {
self.driver = driver
self.framesPerSecond = framesPerSecond
self.update = update
}
public func invalidate() {
self.isValid = false
}
}
private final class RequestContext {
weak var link: LinkImpl?
let framesPerSecond: FramesPerSecond
var lastDuration: Double = 0.0
init(link: LinkImpl, framesPerSecond: FramesPerSecond) {
self.link = link
self.framesPerSecond = framesPerSecond
}
}
private var displayLink: CADisplayLink?
private var requests: [RequestContext] = []
private var isInForeground: Bool = false
private var isProcessingEvent: Bool = false
private var isUpdateRequested: Bool = false
private init() {
let _ = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let self else {
return
}
self.isInForeground = true
self.update()
})
let _ = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let self else {
return
}
self.isInForeground = false
self.update()
})
if Bundle.main.bundlePath.hasSuffix(".appex") {
self.isInForeground = true
} else {
switch UIApplication.shared.applicationState {
case .active:
self.isInForeground = true
default:
self.isInForeground = false
}
}
self.update()
}
public func updateForegroundState(_ isActive: Bool) {
if self.isInForeground != isActive {
self.isInForeground = isActive
self.update()
}
}
private func requestUpdate() {
if self.isProcessingEvent {
self.isUpdateRequested = true
} else {
self.update()
}
}
private func update() {
var hasActiveItems = false
var maxFramesPerSecond: FramesPerSecond = .fps(30)
for request in self.requests {
if let link = request.link {
if link.framesPerSecond > maxFramesPerSecond {
maxFramesPerSecond = link.framesPerSecond
}
if link.isValid && !link.isPaused {
hasActiveItems = true
break
}
}
}
if self.isInForeground && hasActiveItems {
let displayLink: CADisplayLink
if let current = self.displayLink {
displayLink = current
} else {
displayLink = CADisplayLink(target: self, selector: #selector(self.displayLinkEvent))
self.displayLink = displayLink
displayLink.add(to: .main, forMode: .common)
}
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
var frameRateRange: CAFrameRateRange
switch maxFramesPerSecond {
case let .fps(fps):
if fps > 60 {
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
} else {
frameRateRange = .default
}
case .max:
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
}
if isIpad {
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
}
if displayLink.preferredFrameRateRange != frameRateRange {
displayLink.preferredFrameRateRange = frameRateRange
print("SharedDisplayLinkDriver: switch to \(frameRateRange)")
}
}
}
displayLink.isPaused = false
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
@objc private func displayLinkEvent(displayLink: CADisplayLink) {
self.isProcessingEvent = true
let duration = displayLink.targetTimestamp - displayLink.timestamp
var removeIndices: [Int]?
loop: for i in 0 ..< self.requests.count {
let request = self.requests[i]
if let link = request.link, link.isValid {
if !link.isPaused {
var itemDuration = duration
switch request.framesPerSecond {
case let .fps(value):
let secondsPerFrame = 1.0 / CGFloat(value)
itemDuration = secondsPerFrame
request.lastDuration += duration
if request.lastDuration >= secondsPerFrame * 0.95 {
//print("item \(link) accepting cycle: \(request.lastDuration - duration) + \(duration) = \(request.lastDuration) >= \(secondsPerFrame)")
} else {
//print("item \(link) skipping cycle: \(request.lastDuration - duration) + \(duration) < \(secondsPerFrame)")
continue loop
}
case .max:
break
}
request.lastDuration = 0.0
link.update(itemDuration)
}
} else {
if removeIndices == nil {
removeIndices = [i]
} else {
removeIndices?.append(i)
}
}
}
if let removeIndices = removeIndices {
for index in removeIndices.reversed() {
self.requests.remove(at: index)
}
if self.requests.isEmpty {
self.isUpdateRequested = true
}
}
self.isProcessingEvent = false
if self.isUpdateRequested {
self.isUpdateRequested = false
self.update()
}
}
public func add(framesPerSecond: FramesPerSecond = .fps(60), _ update: @escaping (CGFloat) -> Void) -> Link {
let link = LinkImpl(driver: self, framesPerSecond: framesPerSecond, update: update)
self.requests.append(RequestContext(link: link, framesPerSecond: framesPerSecond))
self.update()
return link
}
}
public final class DisplayLinkTarget: NSObject {
private let f: (CADisplayLink) -> Void
public init(_ f: @escaping (CADisplayLink) -> Void) {
self.f = f
}
@objc public func event(_ displayLink: CADisplayLink) {
self.f(displayLink)
}
}
public final class DisplayLinkAnimator {
private var displayLink: SharedDisplayLinkDriver.Link?
private let duration: Double
private let fromValue: CGFloat
private let toValue: CGFloat
private let startTime: Double
private let update: (CGFloat) -> Void
private let completion: () -> Void
private var completed = false
public init(duration: Double, from fromValue: CGFloat, to toValue: CGFloat, update: @escaping (CGFloat) -> Void, completion: @escaping () -> Void) {
self.duration = duration
self.fromValue = fromValue
self.toValue = toValue
self.update = update
self.completion = completion
self.startTime = CACurrentMediaTime()
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
self?.tick()
}
self.displayLink?.isPaused = false
}
deinit {
self.displayLink?.isPaused = true
self.displayLink?.invalidate()
}
public func invalidate() {
self.displayLink?.isPaused = true
self.displayLink?.invalidate()
}
@objc private func tick() {
if self.completed {
return
}
let timestamp = CACurrentMediaTime()
var t = (timestamp - self.startTime) / self.duration
t = max(0.0, t)
t = min(1.0, t)
self.update(self.fromValue * CGFloat(1 - t) + self.toValue * CGFloat(t))
if abs(t - 1.0) < Double.ulpOfOne {
self.completed = true
self.displayLink?.isPaused = true
self.completion()
}
}
}
public final class ConstantDisplayLinkAnimator {
private var displayLink: SharedDisplayLinkDriver.Link?
private let update: () -> Void
private var completed = false
public var frameInterval: Int = 1 {
didSet {
self.updateDisplayLink()
}
}
private func updateDisplayLink() {
guard let displayLink = self.displayLink else {
return
}
let _ = displayLink
}
public var isPaused: Bool = true {
didSet {
if self.isPaused != oldValue {
if !self.isPaused && self.displayLink == nil {
let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
self?.tick()
}
self.displayLink = displayLink
self.updateDisplayLink()
}
self.displayLink?.isPaused = self.isPaused
}
}
}
public init(update: @escaping () -> Void) {
self.update = update
}
deinit {
if let displayLink = self.displayLink {
displayLink.isPaused = true
displayLink.invalidate()
}
}
public func invalidate() {
if let displayLink = self.displayLink {
displayLink.isPaused = true
displayLink.invalidate()
}
}
@objc private func tick() {
if self.completed {
return
}
self.update()
}
}