-
-
Save avdyushin/35a5c6d92a08c7e31dfb1961f7d9db3e to your computer and use it in GitHub Desktop.
DisplayLink for OSX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// DisplayLink.swift | |
// | |
// Created by Jose Canepa on 8/18/16. | |
// Copyright © 2016 Jose Canepa. All rights reserved. | |
// Updated by Grigory Avdyushin on 3/6/19. | |
// Copyright © 2019 Grigory Avdyushin. All rights reserved. | |
// | |
import AppKit | |
public class DisplayLink { | |
public class State { | |
func isValidNextState(_ stateClass: AnyClass) -> Bool { | |
fatalError() | |
} | |
} | |
class RunningState: State { | |
override func isValidNextState(_ stateClass: AnyClass) -> Bool { | |
return stateClass == PausedState.self || stateClass == StoppedState.self | |
} | |
} | |
class StoppedState: State { | |
override func isValidNextState(_ stateClass: AnyClass) -> Bool { | |
return stateClass == RunningState.self | |
} | |
} | |
class PausedState: State { | |
override func isValidNextState(_ stateClass: AnyClass) -> Bool { | |
return stateClass == RunningState.self || stateClass == StoppedState.self | |
} | |
} | |
class StateMachine { | |
private let states: [State] | |
private(set) var currentState: State? | |
init(states: [State]) { | |
self.states = states | |
} | |
@discardableResult | |
func enter(_ stateClass: AnyClass) -> Bool { | |
if currentState?.isValidNextState(stateClass) != false /* nil or true */ { | |
currentState = states.first { type(of: $0) == stateClass } | |
return true | |
} | |
return false | |
} | |
} | |
public enum DisplayLinkError: Error { | |
case failedToCreateTimer | |
case failedToConnectToDisplay | |
} | |
public typealias TimerEventHandler = (DisplayLink) -> Void | |
private let timer: CVDisplayLink | |
private let source: DispatchSourceUserDataAdd | |
private let eventHandler: TimerEventHandler | |
private let stateMachine = StateMachine( | |
states: [ | |
StoppedState(), | |
RunningState(), | |
PausedState() | |
] | |
) | |
public var state: State? { | |
return stateMachine.currentState | |
} | |
public var isRunning: Bool { | |
return CVDisplayLinkIsRunning(timer) | |
} | |
public init(queue: DispatchQueue = .main, eventHandler: @escaping TimerEventHandler) throws { | |
self.eventHandler = eventHandler | |
self.source = DispatchSource.makeUserDataAddSource(queue: queue) | |
var timerRef: CVDisplayLink? | |
// Create timer | |
CVDisplayLinkCreateWithActiveCGDisplays(&timerRef) | |
guard let timer = timerRef else { | |
throw DisplayLinkError.failedToCreateTimer | |
} | |
self.timer = timer | |
// Set Output | |
var successLink = CVDisplayLinkSetOutputCallback(timer, { | |
(timer: CVDisplayLink, | |
currentTime: UnsafePointer<CVTimeStamp>, | |
outputTime: UnsafePointer<CVTimeStamp>, | |
_ : CVOptionFlags, | |
_ : UnsafeMutablePointer<CVOptionFlags>, | |
sourceUnsafeRaw: UnsafeMutableRawPointer?) -> CVReturn in | |
// Un-opaque the source | |
if let sourceUnsafeRaw = sourceUnsafeRaw { | |
// Update the value of the source, thus, triggering a handle call on the timer | |
let sourceUnmanaged = Unmanaged<DispatchSourceUserDataAdd>.fromOpaque(sourceUnsafeRaw) | |
sourceUnmanaged.takeUnretainedValue().add(data: 1) | |
} | |
return kCVReturnSuccess | |
}, Unmanaged.passUnretained(source).toOpaque()) | |
guard successLink == kCVReturnSuccess else { | |
throw DisplayLinkError.failedToCreateTimer | |
} | |
// Connect to display | |
successLink = CVDisplayLinkSetCurrentCGDisplay(timer, CGMainDisplayID()) | |
guard successLink == kCVReturnSuccess else { | |
throw DisplayLinkError.failedToConnectToDisplay | |
} | |
// Timer setup | |
source.setEventHandler { [weak self] in | |
guard let self = self else { return } | |
self.eventHandler(self) | |
} | |
stateMachine.enter(StoppedState.self) | |
} | |
deinit { | |
// Release of suspended timer cause a crash in realtime! | |
// void | |
// _dispatch_source_xref_release(dispatch_source_t ds) | |
// { | |
// if (slowpath(DISPATCH_OBJECT_SUSPENDED(ds))) { | |
// // Arguments for and against this assert are within 6705399 | |
// DISPATCH_CLIENT_CRASH("Release of a suspended object"); | |
// } | |
// _dispatch_wakeup(ds); | |
// _dispatch_release(ds); | |
// } | |
if !(state is RunningState) { | |
source.resume() | |
} | |
} | |
/// Starts the timer | |
public func start() { | |
if stateMachine.enter(RunningState.self) { | |
CVDisplayLinkStart(timer) | |
source.resume() | |
} else { | |
assertionFailure() | |
} | |
} | |
/// Pauses the timer, can be restarted aftewards | |
public func pause() { | |
if stateMachine.enter(PausedState.self) { | |
CVDisplayLinkStop(timer) | |
source.suspend() | |
} else { | |
assertionFailure() | |
} | |
} | |
/// Cancels the timer | |
public func stop() { | |
if state is RunningState { | |
CVDisplayLinkStop(timer) | |
source.cancel() | |
} | |
if !stateMachine.enter(StoppedState.self) { | |
assertionFailure() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment