2016-01-05 04:40:43 +00:00
|
|
|
//
|
|
|
|
// MachineDocument.swift
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 04/01/2016.
|
|
|
|
// Copyright © 2016 Thomas Harte. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Cocoa
|
2016-01-15 01:33:22 +00:00
|
|
|
import AudioToolbox
|
2016-01-05 04:40:43 +00:00
|
|
|
|
2016-04-20 01:29:10 +00:00
|
|
|
class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDelegate, NSWindowDelegate {
|
2016-01-05 04:40:43 +00:00
|
|
|
|
2016-06-01 02:32:38 +00:00
|
|
|
lazy var actionLock = NSLock()
|
|
|
|
lazy var drawLock = NSLock()
|
|
|
|
func machine() -> CSMachine! {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-06-01 23:04:07 +00:00
|
|
|
func aspectRatio() -> NSSize {
|
|
|
|
return NSSize(width: 4.0, height: 3.0)
|
|
|
|
}
|
|
|
|
|
2016-03-05 19:45:09 +00:00
|
|
|
@IBOutlet weak var openGLView: CSOpenGLView! {
|
2016-01-05 04:40:43 +00:00
|
|
|
didSet {
|
|
|
|
openGLView.delegate = self
|
|
|
|
openGLView.responderDelegate = self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-18 01:43:39 +00:00
|
|
|
@IBOutlet weak var optionsPanel: NSPanel!
|
2016-04-18 12:21:00 +00:00
|
|
|
@IBAction func showOptions(sender: AnyObject!) {
|
2016-04-18 01:43:39 +00:00
|
|
|
optionsPanel?.setIsVisible(true)
|
|
|
|
}
|
|
|
|
|
2016-06-01 23:04:07 +00:00
|
|
|
var audioQueue : AudioQueue! = nil
|
2016-01-15 01:33:22 +00:00
|
|
|
|
2016-01-05 04:40:43 +00:00
|
|
|
override func windowControllerDidLoadNib(aController: NSWindowController) {
|
|
|
|
super.windowControllerDidLoadNib(aController)
|
|
|
|
|
2016-06-01 23:04:07 +00:00
|
|
|
// establish the output aspect ratio and audio
|
|
|
|
let displayAspectRatio = self.aspectRatio()
|
|
|
|
aController.window?.contentAspectRatio = displayAspectRatio
|
|
|
|
openGLView.performWithGLContext({
|
|
|
|
self.machine().setView(self.openGLView, aspectRatio: Float(displayAspectRatio.width / displayAspectRatio.height))
|
|
|
|
})
|
2016-06-01 02:36:53 +00:00
|
|
|
|
2016-06-01 23:04:07 +00:00
|
|
|
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
|
|
|
let maximumSamplingRate = AudioQueue.preferredSamplingRate()
|
|
|
|
let selectedSamplingRate = self.machine().idealSamplingRateFromRange(NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
|
|
|
audioQueue = AudioQueue(samplingRate: Float64(selectedSamplingRate))
|
2016-06-01 02:36:53 +00:00
|
|
|
self.machine().audioQueue = self.audioQueue
|
2016-06-01 23:04:07 +00:00
|
|
|
self.machine().setAudioSamplingRate(selectedSamplingRate)
|
2016-01-05 04:40:43 +00:00
|
|
|
}
|
|
|
|
|
2016-06-01 02:32:38 +00:00
|
|
|
override func close() {
|
|
|
|
actionLock.lock()
|
|
|
|
drawLock.lock()
|
|
|
|
openGLView.invalidate()
|
|
|
|
openGLView.openGLContext!.makeCurrentContext()
|
|
|
|
actionLock.unlock()
|
|
|
|
drawLock.unlock()
|
|
|
|
|
|
|
|
super.close()
|
|
|
|
}
|
|
|
|
|
2016-01-05 04:40:43 +00:00
|
|
|
var intendedCyclesPerSecond: Int64 = 0
|
2016-04-07 00:46:00 +00:00
|
|
|
private var cycleCountError: Int64 = 0
|
|
|
|
private var lastTime: CVTimeStamp?
|
2016-04-21 02:35:46 +00:00
|
|
|
private var skippedFrames = 0
|
|
|
|
final func openGLView(view: CSOpenGLView, didUpdateToTime time: CVTimeStamp, didSkipPreviousUpdate : Bool, frequency : Double) {
|
2016-04-07 00:46:00 +00:00
|
|
|
if let lastTime = lastTime {
|
|
|
|
// perform (time passed in seconds) * (intended cycles per second), converting and
|
|
|
|
// maintaining an error count to deal with underflow
|
|
|
|
let videoTimeScale64 = Int64(time.videoTimeScale)
|
|
|
|
let videoTimeCount = ((time.videoTime - lastTime.videoTime) * intendedCyclesPerSecond) + cycleCountError
|
|
|
|
cycleCountError = videoTimeCount % videoTimeScale64
|
2016-04-21 02:35:46 +00:00
|
|
|
var numberOfCycles = videoTimeCount / videoTimeScale64
|
2016-01-05 04:40:43 +00:00
|
|
|
|
2016-04-21 02:35:46 +00:00
|
|
|
// if the emulation has fallen behind then silently limit the request;
|
2016-03-20 22:42:37 +00:00
|
|
|
// some actions — e.g. the host computer waking after sleep — may give us a
|
|
|
|
// prohibitive backlog
|
2016-04-21 02:35:46 +00:00
|
|
|
if didSkipPreviousUpdate {
|
|
|
|
skippedFrames++
|
|
|
|
} else {
|
|
|
|
skippedFrames = 0
|
|
|
|
}
|
|
|
|
|
2016-06-03 02:29:09 +00:00
|
|
|
// run for at most three frames up to and until that causes overshoots in the
|
|
|
|
// permitted processing window for at least four consecutive frames, in which
|
|
|
|
// case limit to one
|
|
|
|
numberOfCycles = min(numberOfCycles, Int64(Double(intendedCyclesPerSecond) * frequency * ((skippedFrames > 4) ? 3.0 : 1.0)))
|
2016-04-21 02:35:46 +00:00
|
|
|
runForNumberOfCycles(Int32(numberOfCycles))
|
2016-01-05 04:40:43 +00:00
|
|
|
}
|
2016-04-07 00:46:00 +00:00
|
|
|
lastTime = time
|
2016-01-05 04:40:43 +00:00
|
|
|
}
|
|
|
|
|
2016-06-01 02:32:38 +00:00
|
|
|
// MARK: CSOpenGLViewDelegate
|
|
|
|
func runForNumberOfCycles(numberOfCycles: Int32) {
|
|
|
|
if actionLock.tryLock() {
|
|
|
|
self.machine().runForNumberOfCycles(numberOfCycles)
|
|
|
|
actionLock.unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func openGLView(view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
|
|
|
|
if drawLock.tryLock() {
|
|
|
|
self.machine().drawViewForPixelSize(view.backingSize, onlyIfDirty: onlyIfDirty)
|
|
|
|
drawLock.unlock()
|
|
|
|
}
|
|
|
|
}
|
2016-01-05 04:40:43 +00:00
|
|
|
|
|
|
|
// MARK: CSOpenGLViewResponderDelegate
|
|
|
|
func keyDown(event: NSEvent) {}
|
|
|
|
func keyUp(event: NSEvent) {}
|
|
|
|
func flagsChanged(newModifiers: NSEvent) {}
|
2016-06-05 01:43:50 +00:00
|
|
|
|
|
|
|
// MARK: NSDocument overrides
|
|
|
|
override func dataOfType(typeName: String) throws -> NSData {
|
|
|
|
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
|
|
|
}
|
2016-01-05 04:40:43 +00:00
|
|
|
}
|