DisplayLink.swift 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. //
  2. // DisplayLink.swift
  3. // Kingfisher
  4. //
  5. // Created by yeatse on 2024/1/9.
  6. //
  7. // Copyright (c) 2024 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. #if !os(watchOS)
  27. #if canImport(UIKit)
  28. import UIKit
  29. #else
  30. import AppKit
  31. import CoreVideo
  32. #endif
  33. protocol DisplayLinkCompatible: AnyObject, Sendable {
  34. var isPaused: Bool { get set }
  35. var preferredFramesPerSecond: NSInteger { get }
  36. var timestamp: CFTimeInterval { get }
  37. var duration: CFTimeInterval { get }
  38. func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode)
  39. func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode)
  40. func invalidate()
  41. }
  42. #if !os(macOS)
  43. extension UIView {
  44. func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible {
  45. return CADisplayLink(target: target, selector: selector)
  46. }
  47. }
  48. #if compiler(>=6)
  49. extension CADisplayLink: DisplayLinkCompatible, @retroactive @unchecked Sendable {}
  50. #else
  51. extension CADisplayLink: DisplayLinkCompatible, @unchecked Sendable {}
  52. #endif
  53. #else
  54. extension NSView {
  55. func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible {
  56. #if swift(>=5.9) // macOS 14 SDK is included in Xcode 15, which comes with swift 5.9. Add this check to make old compilers happy.
  57. if #available(macOS 14.0, *) {
  58. return displayLink(target: target, selector: selector)
  59. } else {
  60. return DisplayLink(target: target, selector: selector)
  61. }
  62. #else
  63. return DisplayLink(target: target, selector: selector)
  64. #endif
  65. }
  66. }
  67. #if swift(>=5.9)
  68. @available(macOS 14.0, *)
  69. extension CADisplayLink: DisplayLinkCompatible {
  70. var preferredFramesPerSecond: NSInteger { return 0 }
  71. }
  72. #if compiler(>=6)
  73. @available(macOS 14.0, *)
  74. extension CADisplayLink: @retroactive @unchecked Sendable { }
  75. #else // compiler(>=6)
  76. @available(macOS 14.0, *)
  77. extension CADisplayLink: @unchecked Sendable { }
  78. #endif // compiler(>=6)
  79. #endif // swift(>=5.9)
  80. final class DisplayLink: DisplayLinkCompatible, @unchecked Sendable {
  81. private var link: CVDisplayLink?
  82. private var target: Any?
  83. private var selector: Selector?
  84. private var schedulers: [RunLoop: [RunLoop.Mode]] = [:]
  85. var preferredFramesPerSecond: NSInteger = 0
  86. var timestamp: CFTimeInterval = 0
  87. var duration: CFTimeInterval = 0
  88. init(target: Any, selector: Selector) {
  89. self.target = target
  90. self.selector = selector
  91. CVDisplayLinkCreateWithActiveCGDisplays(&link)
  92. if let link = link {
  93. CVDisplayLinkSetOutputHandler(link) { displayLink, inNow, inOutputTime, flagsIn, flagsOut in
  94. self.displayLinkCallback(
  95. displayLink, inNow: inNow, inOutputTime: inOutputTime, flagsIn: flagsIn, flagsOut: flagsOut
  96. )
  97. }
  98. }
  99. }
  100. deinit {
  101. self.invalidate()
  102. }
  103. private func displayLinkCallback(_ link: CVDisplayLink,
  104. inNow: UnsafePointer<CVTimeStamp>,
  105. inOutputTime: UnsafePointer<CVTimeStamp>,
  106. flagsIn: CVOptionFlags,
  107. flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn
  108. {
  109. let outputTime = inOutputTime.pointee
  110. DispatchQueue.main.async {
  111. guard let selector = self.selector, let target = self.target else { return }
  112. if outputTime.videoTimeScale != 0 {
  113. self.duration = CFTimeInterval(Double(outputTime.videoRefreshPeriod) / Double(outputTime.videoTimeScale))
  114. }
  115. if self.timestamp != 0 {
  116. for scheduler in self.schedulers {
  117. scheduler.key.perform(selector, target: target, argument: nil, order: 0, modes: scheduler.value)
  118. }
  119. }
  120. self.timestamp = CFTimeInterval(Double(outputTime.hostTime) / 1_000_000_000)
  121. }
  122. return kCVReturnSuccess
  123. }
  124. var isPaused: Bool = true {
  125. didSet {
  126. guard let link = link else { return }
  127. if isPaused {
  128. if CVDisplayLinkIsRunning(link) {
  129. CVDisplayLinkStop(link)
  130. }
  131. } else {
  132. if !CVDisplayLinkIsRunning(link) {
  133. CVDisplayLinkStart(link)
  134. }
  135. }
  136. }
  137. }
  138. func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode) {
  139. assert(runLoop == .main)
  140. schedulers[runLoop, default: []].append(mode)
  141. }
  142. func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode) {
  143. schedulers[runLoop]?.removeAll { $0 == mode }
  144. if let modes = schedulers[runLoop], modes.isEmpty {
  145. schedulers.removeValue(forKey: runLoop)
  146. }
  147. }
  148. func invalidate() {
  149. schedulers = [:]
  150. isPaused = true
  151. target = nil
  152. selector = nil
  153. if let link = link {
  154. CVDisplayLinkSetOutputHandler(link) { _, _, _, _, _ in kCVReturnSuccess }
  155. }
  156. }
  157. }
  158. #endif
  159. #endif