15

I'm trying to create a main render loop for a Swift OS X OpenGL application, but I can't find any examples on the web, and can't figure out the interaction with the Objective C API.

Here's code during initialization of my subclass of NSOpenGLView:

  var udl : Unmanaged<CVDisplayLink>?
  CVDisplayLinkCreateWithActiveCGDisplays(&udl)
  var displayLink: CVDisplayLink = udl!.takeRetainedValue()  // I guess

  // The two following lines give errors that the type isn't convertible 
  // to the declared type:
  let cb: CVDisplayLinkOutputCallback = dlCallback  // ERROR: type not convertible
  let sp: UnsafeMutablePointer<Void> = &self        // ERROR: type not convertible
  CVDisplayLinkSetOutputCallback(displayLink, cb, sp)

  let cglContext = openGLContext.CGLContextObj
  let cglPixelFormat = pixelFormat.CGLPixelFormatObj
  CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink, cglContext, cglPixelFormat)

  CVDisplayLinkStart(displayLink)

and here's my callback function. I don't know how to extract the pointer back to my view class from the opaque pointer I passed to CVDisplayLinkSetOutputCallback (or tried to).

func dlCallback(displayLink: CVDisplayLink!,
            inNow: UnsafePointer<CVTimeStamp>,
            inOutputTime: UnsafePointer<CVTimeStamp>,
            flagsIn: CVOptionFlags,
            flagsOut: UnsafeMutablePointer<CVOptionFlags>,
            context: UnsafeMutablePointer<Void>) -> CVReturn {
  let that = UnsafeMutablePointer<MyView>(context)  // Just a guess, but no
  that.render()  // ERROR: no such method
}

I'd like to understand how to do this.

If I should use some other main render loop instead based on timers, I guess I can do that instead.

Grumdrig
  • 16,588
  • 14
  • 58
  • 69

4 Answers4

24

Updated for Swift 3.0 - see answer end

Instead of erasing my previous answer, which I think is useful for those wanting to utilize Obj-C and Swift together, I will provide a second alternate answer which utilizes pure Swift Code. Now that Swift 2.0 is released, we can utilize the CFunctionPointer to pass Swift functions and closures as C API arguments. Here is the code with comments.

//
//  SwiftOpenGLView.swift
//  Swift CVDisplayLink
//
//  Created by Myles La Verne Schultz on 10/17/15.
//  Copyright © 2015 MyKo. All rights reserved.
//

import Cocoa
import OpenGL.GL3


class SwiftOpenGLView: NSOpenGLView {

    var displayLink: CVDisplayLink?

    required init?(coder: NSCoder) {

        //  Call the super before setting the pixelFormat and openGLContext so that the super does not override 
        //  our custom versions of these properties.
        super.init(coder: coder)

        //  Create a pixel format and context and set them to the view's pixelFormat and openGLContext properties.
        let attributes: [NSOpenGLPixelFormatAttribute] = [
            UInt32(NSOpenGLPFAAccelerated),
            UInt32(NSOpenGLPFAColorSize), UInt32(32),
            UInt32(NSOpenGLPFADoubleBuffer),
            UInt32(NSOpenGLPFAOpenGLProfile),
            UInt32(NSOpenGLProfileVersion3_2Core),
            UInt32(0)
        ]
        guard let pixelFormat = NSOpenGLPixelFormat(attributes: attributes) else {
            Swift.print("pixel format could not be created")
            return
        }
        self.pixelFormat = pixelFormat

        guard let context = NSOpenGLContext(format: pixelFormat, shareContext: nil) else {
            Swift.print("context could not be created")
            return
        }
        self.openGLContext = context

        //  Tell the view how often we are swaping the buffers, 1 indicates we are using the 60Hz refresh rate (i.e. 60 fps)
        self.openGLContext?.setValues([1], forParameter: .GLCPSwapInterval)

    }

    //  prepareOpenGL is where we set OpenGL state calls before the first render, we will set up the CVDisplayLink here.
    override func prepareOpenGL() {

        //  The callback function is called everytime CVDisplayLink says its time to get a new frame.
        func displayLinkOutputCallback(displayLink: CVDisplayLink, _ inNow: UnsafePointer<CVTimeStamp>, _ inOutputTime: UnsafePointer<CVTimeStamp>, _ flagsIn: CVOptionFlags, _ flagsOut: UnsafeMutablePointer<CVOptionFlags>, _ displayLinkContext: UnsafeMutablePointer<Void>) -> CVReturn {

            /*  The displayLinkContext is CVDisplayLink's parameter definition of the view in which we are working.
                In order to access the methods of a given view we need to specify what kind of view it is as right
                now the UnsafeMutablePointer<Void> just means we have a pointer to "something".  To cast the pointer
                such that the compiler at runtime can access the methods associated with our SwiftOpenGLView, we use
                an unsafeBitCast.  The definition of which states, "Returns the the bits of x, interpreted as having
                type U."  We may then call any of that view's methods.  Here we call drawView() which we draw a
                frame for rendering.  */
            unsafeBitCast(displayLinkContext, SwiftOpenGLView.self).renderFrame()

            //  We are going to assume that everything went well for this mock up, and pass success as the CVReturn
            return kCVReturnSuccess
        }

        //  Grab the a link to the active displays, set the callback defined above, and start the link.
        /*  An alternative to a nested function is a global function or a closure passed as the argument--a local function 
            (i.e. a function defined within the class) is NOT allowed. */
        //  The UnsafeMutablePointer<Void>(unsafeAddressOf(self)) passes a pointer to the instance of our class.
        CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
        CVDisplayLinkSetOutputCallback(displayLink!, displayLinkOutputCallback, UnsafeMutablePointer<Void>(unsafeAddressOf(self)))
        CVDisplayLinkStart(displayLink!)

    }

    //  Method called to render a new frame with an OpenGL pipeline
    func renderFrame() {

        guard let context = self.openGLContext else {
            Swift.print("oops")
            return
        }

        //  Tell OpenGL this is the context we want to draw into and lock the focus.
        context.makeCurrentContext()
        CGLLockContext(context.CGLContextObj)

        //  Lock the focus before making state change calls to OpenGL, or the app gives you a EXC_BAD_ACCESS fault
        //  This float is a changing value we can use to create a simple animation.
        let value = Float(sin(1.00 * CACurrentMediaTime()))
        //  Uses the float to set a clear color that is on the gray scale.
        glClearColor(value, value, value, 1.0)

        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
        //  Flushing sends the context to be used for display, then we can unlock the focus.
        CGLFlushDrawable(context.CGLContextObj)
        CGLUnlockContext(context.CGLContextObj)

    }

    override func drawRect(dirtyRect: NSRect) {

        super.drawRect(dirtyRect)

        // Should drawRect(_:) get called, we want a new frame to be drawn, so call drawView()
        renderFrame()

    }

    deinit {

        //When the view gets destroyed, we don't want to keep the link going.
        CVDisplayLinkStop(displayLink!)

    }

}

Swift 3.0 and CVDisplayLink

A number of changes have been made to pointers in Swift that broke the previous version of this answer. To keep the information current, I am providing the updated version below.

//
//  SwiftOpenGLView_Swift_3_0.swift
//  SwiftOpenGL
//
//  Created by Myles La Verne Schultz on 1/11/17.
//  Copyright © 2017 MyKo. All rights reserved.
//
//  This file is an update to the previous SwiftOpenGLView used
//  to display animated content using the CVDisplayLink.  This
//  version uses Swift 3.0 without the need for a bridging
//  header for the CVDisplayLinkCallback function.  An
//  explanation of the CVTimeStamp is also provided.
//
import Cocoa
import OpenGL.GL3


final class SwiftOpenGLView: NSOpenGLView {

    //  A CVDisplayLink for animating.
    fileprivate var displayLink: CVDisplayLink?

    //  The current time, used to produce varying values to change background color
    fileprivate var currentTime = 0.0

    required init?(coder: NSCoder) {
        super.init(coder: coder)

        let attrs: [NSOpenGLPixelFormatAttribute] = [
            UInt32(NSOpenGLPFAAccelerated),
            UInt32(NSOpenGLPFADoubleBuffer),
            UInt32(NSOpenGLPFAColorSize), UInt32(32),
            UInt32(NSOpenGLPFAOpenGLProfile), UInt32(NSOpenGLProfileVersion3_2Core),
            UInt32(0)
        ]
        guard let pixelFormat = NSOpenGLPixelFormat(attributes: attrs) else {
            Swift.print("pixelFormat could not be constructed")
            return
        }
        self.pixelFormat = pixelFormat
        guard let context = NSOpenGLContext(format: pixelFormat, share: nil) else {
            Swift.print("context could not be constructed")
            return
        }
        self.openGLContext = context

        //  Set the context's swap interval parameter to 60Hz (i.e. 1 frame per swamp)
        self.openGLContext?.setValues([1], for: .swapInterval)

    }

    override func prepareOpenGL() {

        super.prepareOpenGL()

        glClearColor(0.0, 0.0, 0.0, 1.0)

        // ** ** ** ** ** ** ** ** ** //
        // Setup OpenGL pipline here  //
        // ** ** ** ** ** ** ** ** ** //

        /*  Now that the OpenGL pipeline is defined, declare a callback for our CVDisplayLink.
            There are three ways to do this:  declare a function, declare a computed property,
            or declare/pass a closure.  Using each requires subtle changes in the
            CVDisplayLinkSetOutputCallback()'s argument list.  We shall declare a local
            closure of type CVDisplayLinkOutputCallback.
         */
        let displayLinkOutputCallback: CVDisplayLinkOutputCallback = {(displayLink: CVDisplayLink, inNow: UnsafePointer<CVTimeStamp>, inOutputTime: UnsafePointer<CVTimeStamp>, flagsIn: CVOptionFlags, flagsOut: UnsafeMutablePointer<CVOptionFlags>, displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn in

            /*  It's prudent to also have a brief discussion about the CVTimeStamp.
                CVTimeStamp has five properties.  Three of the five are very useful
                for keeping track of the current time, calculating delta time, the
                frame number, and the number of frames per second.  The utility of
                each property is not terribly obvious from just reading the names
                or the descriptions in the Developer dcumentation and has been a
                mystery to many a developer.  Thankfully, CaptainRedmuff on
                StackOverflow asked a question that provided the equation that
                calculates frames per second.  From that equation, we can
                extrapolate the value of each field.

                @hostTime = current time in Units of the "root".  Yeah, I don't know.
                  The key to this field is to understand that it is in nanoseconds
                  (e.g. 1/1_000_000_000 of a second) not units.  To convert it to
                  seconds divide by 1_000_000_000.  Dividing by videoRefreshPeriod
                  and videoTimeScale in a calculation for frames per second yields
                  the appropriate number of frames.  This works as a result of
                  proportionality--dividing seconds by seconds.  Note that dividing
                  by videoTimeScale to get the time in seconds does not work like it
                  does for videoTime.

                  framesPerSecond:
                    (videoTime / videoRefreshPeriod) / (videoTime / videoTimeScale) = 59
                  and
                    (hostTime / videoRefreshPeriod) / (hostTime / videoTimeScale) = 59
                  but
                    hostTime * videoTimeScale ≠ seconds, but Units = seconds * (Units / seconds) = Units

              @rateScalar = ratio of "rate of device in CVTimeStamp/unitOfTime" to
                the "Nominal Rate".  I think the "Nominal Rate" is
                videoRefreshPeriod, but unfortunately, the documentation doesn't
                just say videoRefreshPeriod is the Nominal rate and then define
                what that means.  Regardless, because this is a ratio, and the fact
                that we know the value of one of the parts (e.g. Units/frame), we
                then know that the "rate of the device" is frame/Units (the units of
                measure need to cancel out for the ratio to be a ratio).  This
                makes sense in that rateScalar's definition tells us the rate is
                "measured by timeStamps".  Since there is a frame for every
                timeStamp, the rate of the device equals CVTimeStamp/Unit or
                frame/Unit.  Thus,

                  rateScalar = frame/Units : Units/frame

              @videoTime = the time the frame was created since computer started up.
                If you turn your computer off and then turn it back on, this timer
                returns to zero.  The timer is paused when you put your computer to
                sleep.  This value is in Units not seconds.  To get the number of
                seconds this value represents, you have to apply videoTimeScale.

              @videoRefreshPeriod = the number of Units per frame (i.e. Units/frame)
                This is useful in calculating the frame number or frames per second.
                The documentation calls this the "nominal update period" and I am
                pretty sure that is quivalent to the aforementioned "nominal rate".
                Unfortunately, the documetation mixes naming conventions and this
                inconsistency creates confusion.

                  frame = videoTime / videoRefreshPeriod

              @videoTimeScale = Units/second, used to convert videoTime into seconds
                and may also be used with videoRefreshPeriod to calculate the expected
                framesPerSecond.  I say expected, because videoTimeScale and
                videoRefreshPeriod don't change while videoTime does change.  Thus,
                to calculate fps in the case of system slow down, one would need to
                use videoTime with videoTimeScale to calculate the actual fps value.

                  seconds = videoTime / videoTimeScale

                  framesPerSecondConstant = videoTimeScale / videoRefreshPeriod (this value does not change if their is system slowdown)

            USE CASE 1: Time in DD:HH:mm:ss using hostTime
              let rootTotalSeconds = inNow.pointee.hostTime
              let rootDays = inNow.pointee.hostTime / (1_000_000_000 * 60 * 60 * 24) % 365
              let rootHours = inNow.pointee.hostTime / (1_000_000_000 * 60 * 60) % 24
              let rootMinutes = inNow.pointee.hostTime / (1_000_000_000 * 60) % 60
              let rootSeconds = inNow.pointee.hostTime / 1_000_000_000 % 60
              Swift.print("rootTotalSeconds: \(rootTotalSeconds) rootDays: \(rootDays) rootHours: \(rootHours) rootMinutes: \(rootMinutes) rootSeconds: \(rootSeconds)")

            USE CASE 2: Time in DD:HH:mm:ss using videoTime
              let totalSeconds = inNow.pointee.videoTime / Int64(inNow.pointee.videoTimeScale)
              let days = (totalSeconds / (60 * 60 * 24)) % 365
              let hours = (totalSeconds / (60 * 60)) % 24
              let minutes = (totalSeconds / 60) % 60
              let seconds = totalSeconds % 60
              Swift.print("totalSeconds: \(totalSeconds) Days: \(days) Hours: \(hours) Minutes: \(minutes) Seconds: \(seconds)")

              Swift.print("fps: \(Double(inNow.pointee.videoTimeScale) / Double(inNow.pointee.videoRefreshPeriod)) seconds: \(Double(inNow.pointee.videoTime) / Double(inNow.pointee.videoTimeScale))")
             */

            /*  The displayLinkContext in CVDisplayLinkOutputCallback's parameter list is the
                view being driven by the CVDisplayLink.  In order to use the context as an
                instance of SwiftOpenGLView (which has our drawView() method) we need to use
                unsafeBitCast() to cast this context to a SwiftOpenGLView.
             */

            let view = unsafeBitCast(displayLinkContext, to: SwiftOpenGLView.self)
            //  Capture the current time in the currentTime property.
            view.currentTime = inNow.pointee.videoTime / Int64(inNow.pointee.videoTimeScale)
            view.drawView()

            //  We are going to assume that everything went well, and success as the CVReturn
            return kCVReturnSuccess
        }

        /*  Grab the a link to the active displays, set the callback defined above, and start
            the link.  An alternative to a nested function is a global function or a closure
            passed as the argument--a local function (i.e. a function defined within the
            class) is NOT allowed.  The
            UnsafeMutableRawPointer(unmanaged.passUnretained(self).toOpaque()) passes a
            pointer to an instance of SwiftOpenGLView.  UnsafeMutableRawPointer is a new type
            Swift 3.0 that does not require type definition at its creation.  For greater
            detail place the Swift Evolution notes at https://github.com/apple/swift-evolution/blob/master/proposals/0107-unsaferawpointer.md
        */
        CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
        CVDisplayLinkSetOutputCallback(displayLink!, displayLinkOutputCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
        CVDisplayLinkStart(displayLink!)

        //  Test render

    }

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        // Drawing code here.
        // This call is not entirely necessary as the view is already
        // set to draw with every screen refresh.  Were we to have
        // used the view's display() function, then this object's
        // draw(_:) would actually be called and this our drawView()
        // within it.  As it is now, it's not based on our implementation.
        drawView()

    }

    fileprivate func drawView() {

        //  Grab a context, make it the active context for drawing, and then lock the focus
        //  before making OpenGL calls that change state or data within objects.
        guard let context = self.openGLContext else {
            //  Just a filler error
            Swift.print("oops")
            return
        }

        context.makeCurrentContext()
        CGLLockContext(context.cglContextObj!)

        value = sin(currentTime)
        glClearColor(value, value, value, 1.0)

        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))

        //  glFlush() is replaced with CGLFlushDrawable() and swaps the buffer being displayed
        CGLFlushDrawable(context.cglContextObj!)
        CGLUnlockContext(context.cglContextObj!)
    }

    deinit {
        //  Stop the display link.  A better place to stop the link is in
        //  the viewController or windowController within functions such as
        //  windowWillClose(_:)
        CVDisplayLinkStop(displayLink!)
    }

}

Source code located at GitHub

CVDisplayLink Driven Animation in Swift

Buggus Mageevers
  • 585
  • 5
  • 14
  • swift 3 version of this answer? – Sentry.co Jan 11 '17 at 20:48
  • 1
    I do have a working version. You can see working code on my GitHub under the SwiftOpenGLTutorials repository. Every tutorial using CVDisplayLink is a working Swift 3 version. I haven't updated the answer or the GitHub of the CVDisplayLink repository, it I will now that you've mentioned it. Should be ready sometime later this evening. – Buggus Mageevers Jan 11 '17 at 20:52
  • 2
    Awesome! I corrected my own code now going from swift 2 -> 3 I had to pay attention to ``UnsafeMutablePointer(unsafeAddressOf(self))`` is now: ``UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())`` and ``displayLinkContext: UnsafeMutableRawPointer?`` is now: ``displayLinkContext: UnsafeMutableRawPointer?`` These were not corrected in the auto migration for swift 3. Everything else auto migrated for me – Sentry.co Jan 12 '17 at 00:00
  • 1
    Very true. I had to do the same myself a few months ago when I updated the tutorials I had written to Swift 3.0. I've updated the answer to show a Swift 3.0 version. The GitHub link to the source code is also provided. – Buggus Mageevers Jan 12 '17 at 02:51
  • @Buggy the link you've provided appears broken: http://opengl-swift-and-cvdisplaylink/SwiftOpenGL/SwiftOpenGLView_Swift_3_0.swift GitHub is not even in the URL! – Ilias Karim Mar 24 '17 at 03:03
  • @ILI4SK4RIM I thought I responded to your comment back in March, but obviously not. I double checked the link just now and it has been fixed. Thank you for the alert. – Buggus Mageevers Jul 26 '17 at 01:43
5

You are not alone in your endeavor to make CVDisplayLink work in Swift Code. I have been trying to make this work and have realized something through my research, Swift doesn't have the capability to send the arguments to C API parameters to make CVDisplayLink work. I posted my answer to the Apple Developer forums as well, and if you don't mind, I'll share it with you.

FINAL EDIT

I have posted fully implemented Swift and Obj-C files to GitHub

The repository contains:

  1. SwiftOpenGLView.swift (subclass of NSOpenGLView in Swift)
  2. CVDisplayLinkCallbackFunction.h and .m (Obj-C class for the callback function
  3. SwiftOpenGL-Bridging-Header.h (to assess the callback function in Swift)
  4. Appdelegate.swift (for stopping the CVDisplayLink on App termination

The files contain a number of comments to help the reader better understand how the code works. OpenGL and CVDisplayLink aren't the easiest API's on Mac to learn. Hopefully, these files will help expedite that process. Input is welcome, but please, remember to be nice--I worked hard on these files. The same files are shown below.


SwiftOpenGLView.swift

import Cocoa
import OpenGL.GL3
import QuartzCore.CVDisplayLink


@objc class SwiftOpenGLView: NSOpenGLView {

    var displayLink: CVDisplayLink?

    required init?(coder: NSCoder) {

        //  CVDisplayLinkCreateActiveCGDisplays() says we are enabling all useable 
        //  delays to show our content.  Pass in the displayLink porterty
        //  to capture the link.

        CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)

        super.init(coder: coder)

        //  some OpenGL setup
        //  NSOpenGLPixelFormatAttribute is a typealias for UInt32 in Swift, cast each attribute
        //  Set the view's PixelFormat and Context to the custom pixelFormat and context

        let attrs: [NSOpenGLPixelFormatAttribute] = [
            UInt32(NSOpenGLPFAAccelerated),
            UInt32(NSOpenGLPFAColorSize), UInt32(32),
            UInt32(NSOpenGLPFADoubleBuffer),
            UInt32(NSOpenGLPFAOpenGLProfile),
            UInt32( NSOpenGLProfileVersion3_2Core),
            UInt32(0)
        ]
        let pixelFormat = NSOpenGLPixelFormat(attributes: attrs)
        self.pixelFormat = pixelFormat
        let context = NSOpenGLContext(format: pixelFormat, shareContext: nil)
        self.openGLContext = context

        //  Set the swaping interval parameter on the context, setValues:forParameter: is expecting multiple values--use an array
        //  In Swift, context parameters are accessed though the NSOpenGLContextParameter enum, use dot syntax to access the swap interval

        var swapInterval: [GLint] = [1]
        self.openGLContext.setValues(swapInterval, forParameter: .GLCPSwapInterval)

        //  CVDLCallbackFunctionPointer() is a C function declared in CVDisplayLinkCallbackFunction.h
        //  It returns a pointer to our callback:  CVDisplayLinkOutputCallback
        //  The third parameter takes an UnsafeMutablePointer<Void> and our argument needs to be our view (ie self)
        //  We have already stated this type of parameter requires the address of operator '&'
        //  We can't use'&' on out object, but we can still access the pointer using unsafeAddressOf()
        //  However, this address/pointer can't be passed as is--you have to cast to UnsafeMutablePointer<T> (where T is our class)
        //  To se the current display from our OpenGL context, we retrieve the pixelFormat and context as CoreGraphicsLayer objects
        //  Start the CVDisplayLink, note that we need to stop the displayLink when we are done --> done in APPDELEGATE.SWIFT!!!

        CVDisplayLinkSetOutputCallback(displayLink!, CVDLCallbackFunctionPointer(), UnsafeMutablePointer<SwiftOpenGLView>(unsafeAddressOf(self)))
        let cglPixelFormat = self.pixelFormat?.CGLPixelFormatObj
        let cglContext = self.openGLContext.CGLContextObj
        CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink!, cglContext, cglPixelFormat!)
        CVDisplayLinkStart(displayLink!)
    }

    //  Called by the callback function to ask our model to render out a frame for our context
    //  We have to cast from an UnsafePointer<CVTimeStamp> to an UnsafeMutablePointer<CVTimeStamp>

    func getFrameForTime(outputTime: UnsafePointer<CVTimeStamp>)->CVReturn {
        CVDisplayLinkGetCurrentTime(displayLink!, UnsafeMutablePointer<CVTimeStamp>(outputTime))

        //  For development purpose, calculate the frames per second using the CVTimeStamp passed to the callback function
        //  CVTimeStamp is a C struct with several members that are accessed by going straight to their memory location with .memory
        //  'command' + 'click' on CVTimeStamp to see the struct's definition

        let fps = (outputTime.memory.rateScalar * Double(outputTime.memory.videoTimeScale) / Double(outputTime.memory.videoRefreshPeriod))
        println("FPS:\t \(fps)")

        //  It's time to draw, request the rendered frame

        drawView()

        return kCVReturnSuccess.value
    }

    override func prepareOpenGL() {

        //  Setup OpenGL

        glClearColor(0.0, 0.0, 0.0, 1.0)

        //  Run a test render

        drawView()

    }

    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)

        // Drawing code here.

        drawView()

    }

    func drawView() {

        //  Grab a context from our view and make it current for drawing into
        //  CVDisplayLink uses a separate thread, lock focus or our context for thread safety

        let context = self.openGLContext
        context.makeCurrentContext()
        CGLLockContext(context.CGLContextObj)

        //  Clear the context, set up the OpenGL shader program(s), call drawing commands
        //  OpenGL targets and such are UInt32's, cast them before sending in the OpenGL function

        glClear(UInt32(GL_COLOR_BUFFER_BIT))

        //  We're using a double buffer, call CGLFlushDrawable() to swap the buffer
        //  We're done drawing, unlock the context before moving on

        CGLFlushDrawable(context.CGLContextObj)
        CGLUnlockContext(context.CGLContextObj)

    }

}

CVDisplayLinkCallbackFunction.h

@import Foundation;
@import QuartzCore.CVDisplayLink;


@interface CVDisplayLinkCallbackFunction : NSObject

CVDisplayLinkOutputCallback CVDLCallbackFunctionPointer();

@end

CVDisplayLinkCallbackFunction.m

#import "CVDisplayLinkCallbackFunction.h"
#import "SwiftOpenGL-Swift.h"


@implementation CVDisplayLinkCallbackFunction


CVDisplayLinkOutputCallback CVDLCallbackFunctionPointer()
{
    return CVDLCallbackFunction;
}


CVReturn CVDLCallbackFunction( CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext )
{
    //  Tell CVDisplayLink to call getFrameForTime: (in SwiftOpenGLView) with the provided CVTimeStamp
    //  The function returns a result which can be checked for success

    CVReturn result = [(__bridge SwiftOpenGLView*)displayLinkContext getFrameForTime:inOutputTime];

    return result;
}

@end

SwiftOpenGL-Bridging-Header.h

#import "CVDisplaylinkCallbackFunction.h"

AppDelegate.swift

import Cocoa
import QuartzCore.CVDisplayLink

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {



    func applicationDidFinishLaunching(aNotification: NSNotification) {
        // Insert code here to initialize your application
    }

    func applicationWillTerminate(aNotification: NSNotification) {
        // Insert code here to tear down your application

        //  Grab the current window in our app, and from that grab the subviews of the attached viewController
        //  Cycle through that array to get our SwiftOpenGLView instance

        let windowController = NSApplication.sharedApplication().mainWindow?.windowController() as? NSWindowController
        let views = windowController?.contentViewController?.view.subviews as [NSView]
        for view in views {
            if let aView = view as? SwiftOpenGLView {
                println("Checking if CVDisplayLink is running")
                if let running = CVDisplayLinkIsRunning(aView.displayLink) as Boolean? {
                    println("Stopping CVDisplayLink")
                    let result = CVDisplayLinkStop(aView.displayLink)
                    if result == kCVReturnSuccess.value { println("CVDisplayLink stopped\n\tCode: \(result)") }
                }
            }
        }
    }

}

My hope is that with the advances they have made in Swift 1.2, they will be making these C API's "workable" while coding completely in Swift. Perhaps at this year's WWDC, they'll annouce that you can use all of the C API's in swift without having to use the Obj-C side. Better yet, it would be awesome if the C API's could be re-written in Swift. Personally, I think that is where Apple is intending to take Swift--a cross platform sweep to redefine that frameworks in Swift. I'm not an engineer though, so maybe that isn't what they plan on doing. However, if it isn't... why make Swift in the first place?

Here's hoping that CVDisplayLink becomes available for coding within Swift in the near future.

Buggus Mageevers
  • 585
  • 5
  • 14
  • This doesn't appear to work anymore in Swift 2.0 - there's a type error on `CVDisplayLinkCreateWithActiveCGDisplays(&displayLinkPointer)` - any idea how this should be updated? – sak Oct 16 '15 at 08:59
  • Yes, actually, now the way to get this to work in Swift 2.0 is to do the whole thing in Swift and use the CFuntionPointer. I'll post the code later. I have to to work now. – Buggus Mageevers Oct 16 '15 at 10:51
  • So I was able to solve my own problem - it looks like with the API changes, it's enough to replace that line with `CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)` where `displayLink` is an instance of `CGLDisplayLinkRef` – sak Oct 17 '15 at 11:00
  • 1
    I added a second alternative answer below which utilizes the CFunctionPointer and allows you to write using only Swift. Yes, you're right, now you just send the displayLink property straight into the CVDisplayLInkCreateWithActiveCGDisplays() function. I've edited the original answer too so future visitors don't get confused. Thank you for catching that change! – Buggus Mageevers Oct 19 '15 at 14:22
3

You can get this to work with no Objective-C intermediary by using NSTimer instead of CVDisplayLink. This is the old - but not deprecated - way of doing things as described in this Apple Technote.

CVDisplayLink adds better synching to the screen hardware refresh. But it comes at the cost of multithreading issues and - as @Buggy has explained - Swift function pointer issues. And in the words of an Apple Engineer

Hmm.... even being the person who wrote CVDisplayLink originally, if I were writing a game-like app, I'd probably just use a continuously firing NSTimer and let the VBL stuff throttle the actual frame rate.

Here's an outline of how you can work this using NSTimer...

(1) called from your init method

func setupView() {
        var attribs : [NSOpenGLPixelFormatAttribute] = [
             //set up your NSOpenGLPixelFormat attributes here
        ]
        var pix : NSOpenGLPixelFormat = NSOpenGLPixelFormat(attributes: attribs)
        self.pixelFormat = pix;
        self.renderTimer = NSTimer(timeInterval: 0.001
            , target: self
            , selector: "timerFired"
            , userInfo: nil
            , repeats: true)


    NSRunLoop.currentRunLoop().addTimer(self.renderTimer!
        , forMode:NSDefaultRunLoopMode)
    NSRunLoop.currentRunLoop().addTimer(self.renderTimer!
        , forMode:NSEventTrackingRunLoopMode)

    }

(2) prepareForOpenGL() override...

    override func prepareOpenGL() {
        super.prepareOpenGL()
        self.openGLContext.makeCurrentContext()
        var swapInterval : GLint = 1 // request vsync
        self.openGLContext.setValues(&swapInterval
           , forParameter: NSOpenGLContextParameter.GLCPSwapInterval)

(3) timer function (triggers a system call to drawRect):

func timerFired() {
    self.display()
}

override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        // Drawing code here.
}

I do wonder, though, about the efficacy of a strictly Swift-only openGL environment. Swift was, after all, introduced as 'Objective C without the C'. OpenGL is a C API, so we should expect a struggle to ensue. My own preference is to keep my OpenGL code in C++ with UI calls bridged via Objective-C. The openGL side retains maximal portability, and I have had some success with projects that target iOS, OSX and Windows using this approach.

If you are willing to have Swift float on a layer above Objective-C, this project shows how to get a Swift-talking openGL view going. The swift code is provided as a class extension to an Objective-C NSOpenGLView subclass, with the Objective-C side setting up the CVDisplayLink.

foundry
  • 31,615
  • 9
  • 90
  • 125
  • This isn’t really the best way to create a render loop; using CVDisplayLink instead of NSTimer is important for actually synchronizing the rendering with the display without running too fast, too slow, or unsynchronized. – Chris Hanson Apr 29 '23 at 04:44
1

Ok, so there is a pure-Swift way to use CVDisplayLink, but you have to jump through some unsafeBitCast hoops. (Based on code I found here)

Take your Swift block/closure, cast it to the correct @objc_block type, then cast it to type CVDisplayLinkCallback

You can paste this into a Playground to see it working. The important code is in DisplayLinkSetOutputCallback

typealias DisplayLinkCallback = @objc_block ( CVDisplayLink!, UnsafePointer<CVTimeStamp>, UnsafePointer<CVTimeStamp>, CVOptionFlags, UnsafeMutablePointer<CVOptionFlags>, UnsafeMutablePointer<Void>)->Void

func DisplayLinkSetOutputCallback( displayLink:CVDisplayLink, callback:DisplayLinkCallback )
{
    let block:DisplayLinkCallback = callback
    let myImp = imp_implementationWithBlock( unsafeBitCast( block, AnyObject.self ) )
    let callback = unsafeBitCast( myImp, CVDisplayLinkOutputCallback.self )

    CVDisplayLinkSetOutputCallback( displayLink, callback, UnsafeMutablePointer<Void>() )
}


let dl:CVDisplayLink? = {
    var linkRef:Unmanaged<CVDisplayLink>?
    CVDisplayLinkCreateWithActiveCGDisplays( &linkRef )

    return linkRef?.takeUnretainedValue()
}()

let callback = { (
    _:CVDisplayLink!,
    _:UnsafePointer<CVTimeStamp>,
    _:UnsafePointer<CVTimeStamp>,
    _:CVOptionFlags,
    _:UnsafeMutablePointer<CVOptionFlags>,
    _:UnsafeMutablePointer<Void>)->Void in

    println("yep")

}

DisplayLinkSetOutputCallback( dl!, callback )
CVDisplayLinkStart( dl! )

NSRunLoop.mainRunLoop().run()
nielsbot
  • 15,922
  • 4
  • 48
  • 73