Files
Mike Renoir 7253a1cc40 bugfixes
2024-01-19 16:42:32 +04:00

513 lines
14 KiB
Swift

//
// DisplayLinkAnimator.swift
// TGUIKit
//
// Created by Mikhail Filimonov on 30/09/2019.
// Copyright © 2019 Telegram. All rights reserved.
//
import Cocoa
import SwiftSignalKit
import AppKit
class DisplayLink
{
let timer : CVDisplayLink
let source : DispatchSourceUserDataAdd
var callback : Optional<() -> ()> = nil
var running : Bool { return CVDisplayLinkIsRunning(timer) }
init?(onQueue queue: DispatchQueue = DispatchQueue.main)
{
source = DispatchSource.makeUserDataAddSource(queue: queue)
var timerRef : CVDisplayLink? = nil
var successLink = CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &timerRef)
if let timer = timerRef
{
successLink = CVDisplayLinkSetOutputCallback(timer, { (timer : CVDisplayLink, currentTime : UnsafePointer<CVTimeStamp>, outputTime : UnsafePointer<CVTimeStamp>, _ : CVOptionFlags, _ : UnsafeMutablePointer<CVOptionFlags>, sourceUnsafeRaw : UnsafeMutableRawPointer?) -> CVReturn in
if let sourceUnsafeRaw = sourceUnsafeRaw {
let sourceUnmanaged = Unmanaged<DispatchSourceUserDataAdd>.fromOpaque(sourceUnsafeRaw)
sourceUnmanaged.takeUnretainedValue().add(data: 1)
}
return kCVReturnSuccess
}, Unmanaged.passUnretained(source).toOpaque())
guard successLink == kCVReturnSuccess else
{
NSLog("Failed to create timer with active display")
return nil
}
successLink = CVDisplayLinkSetCurrentCGDisplay(timer, CGMainDisplayID())
guard successLink == kCVReturnSuccess else
{
return nil
}
self.timer = timer
} else {
return nil
}
source.setEventHandler(handler: { [weak self] in
self?.callback?()
})
}
var timestamp: TimeInterval {
return CVDisplayLinkGetActualOutputVideoRefreshPeriod(self.timer)
}
func start() {
guard !running else { return }
CVDisplayLinkStart(timer)
source.resume()
}
func cancel()
{
guard running else { return }
CVDisplayLinkStop(timer)
source.cancel()
}
deinit
{
if running
{
cancel()
}
}
}
public protocol SharedDisplayLinkDriverLink: AnyObject {
var isPaused: Bool { get set }
func invalidate()
}
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: DisplayLink?
private var requests: [RequestContext] = []
private var isInForeground: Bool = false
private var isProcessingEvent: Bool = false
private var isUpdateRequested: Bool = false
private init() {
self.isInForeground = true
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(60)
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: DisplayLink?
if let current = self.displayLink {
displayLink = current
} else {
displayLink = DisplayLink()
displayLink?.callback = { [weak displayLink, weak self] in
if let duration = displayLink?.timestamp {
self?.displayLinkEvent(duration: duration)
}
}
self.displayLink = displayLink
}
displayLink?.start()
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.cancel()
}
}
}
private func displayLinkEvent(duration: TimeInterval) {
self.isProcessingEvent = true
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
}
}
private final class DisplayLinkTarget: NSObject {
private let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
}
@objc func event() {
self.f()
}
}
private struct UnitBezier {
private let ax: Float;
private let bx: Float;
private let cx: Float;
private let ay: Float;
private let by: Float;
private let cy: Float;
init(_ p1x: Float, _ p1y: Float, _ p2x: Float, _ p2y: Float) {
self.cx = 3.0 * p1x;
self.bx = 3.0 * (p2x - p1x) - cx;
self.ax = 1.0 - cx - bx;
self.cy = 3.0 * p1y;
self.by = 3.0 * (p2y - p1y) - cy;
self.ay = 1.0 - cy - by;
}
func sampleCurveX(_ t: Float) -> Float {
return ((ax * t + bx) * t + cx) * t;
}
func sampleCurveY(_ t: Float) -> Float {
return ((ay * t + by) * t + cy) * t;
}
func sampleCurveDerivativeX(_ t: Float) -> Float {
return (3.0 * ax * t + 2.0 * bx) * t + cx;
}
func solveCurveX(_ x: Float, _ epsilon: Float) -> Float{
var t0:Float = 0;
var t1:Float = 0;
var t2:Float = 0;
var x2:Float = 0;
var d2:Float = 0;
for _ in 0 ..< 8 {
t2 = x;
x2 = sampleCurveX(t2) - x;
if (abs (x2) < epsilon) {
return t2;
}
d2 = sampleCurveDerivativeX(t2);
if (abs(d2) < 1e-6) {
break;
}
t2 = t2 - x2 / d2;
}
t0 = 0.0;
t1 = 1.0;
t2 = x;
if (t2 < t0) {
return t0;
}
if (t2 > t1) {
return t1;
}
while (t0 < t1) {
x2 = sampleCurveX(t2);
if (abs(x2 - x) < epsilon) {
return t2;
}
if (x > x2) {
t0 = t2;
} else {
t1 = t2;
}
t2 = (t1 - t0) * 0.5 + t0;
}
return t2;
}
func solve(_ x: Float, _ epsilon: Float) -> Float {
return sampleCurveY(solveCurveX(x, epsilon));
}
}
private func TimingFunctionSolve(_ vec: [Float], _ t: Float, _ eps: Float) -> Float {
let bezier = UnitBezier(vec[0], vec[1], vec[2], vec[3]);
return bezier.solve(t, eps);
}
private func solveEps(_ duration: Float) -> Float {
return (1.0 / (1000.0 * (duration)))
}
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
private var timingControlPoints:[Float] = [0, 0, 0, 0];
public init(duration: Double, from fromValue: CGFloat, to toValue: CGFloat, timingFunction: CAMediaTimingFunction = .init(name: .easeOut), update: @escaping (CGFloat) -> Void, completion: @escaping () -> Void) {
self.duration = duration
self.fromValue = fromValue
self.toValue = toValue
self.update = update
self.completion = completion
var firstTwo:[Float] = [0, 0]
var lastTwo:[Float] = [0, 0]
timingFunction.getControlPoint(at: 1, values: &firstTwo)
timingFunction.getControlPoint(at: 2, values: &lastTwo)
for i in 0 ..< firstTwo.count {
timingControlPoints[i] = firstTwo[i]
}
for i in 0 ..< lastTwo.count {
timingControlPoints[i + firstTwo.count] = lastTwo[i]
}
self.startTime = CACurrentMediaTime()
self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .fps(60), { [weak self] _ in
self?.tick(false)
})
self.tick(true)
}
deinit {
self.displayLink?.invalidate()
self.displayLink = nil
}
public func invalidate() {
self.displayLink?.invalidate()
self.displayLink = nil
}
@objc private func tick(_ isFirst: Bool) {
if self.completed {
return
}
let timestamp = CACurrentMediaTime()
var t = min(timestamp - self.startTime, self.duration) / self.duration
t = max(0.0, t)
t = min(1.0, t)
let solved: Float
if isFirst {
solved = 0
} else {
solved = TimingFunctionSolve(timingControlPoints, Float(t), solveEps(Float(duration)));
}
self.update(self.fromValue * CGFloat(1 - solved) + self.toValue * CGFloat(solved))
if abs(t - 1.0) < Double.ulpOfOne {
self.completed = true
self.displayLink?.invalidate()
self.completion()
}
}
}
public final class ConstantDisplayLinkAnimator {
private var displayLink: SharedDisplayLinkDriver.Link?
private let update: () -> Void
private var completed = false
private let fps: TimeInterval
public var isPaused: Bool = true {
didSet {
if self.isPaused != oldValue {
if self.isPaused {
self.displayLink = nil
} else {
self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .fps(Int(fps)), { [weak self] _ in
self?.tick()
})
}
}
}
}
public init(update: @escaping () -> Void, fps: TimeInterval = 60) {
self.update = update
self.fps = fps
}
deinit {
self.displayLink?.invalidate()
}
public func invalidate() {
self.displayLink?.invalidate()
}
@objc private func tick() {
if self.completed {
return
}
self.update()
}
}