Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
825 views
in Technique[技术] by (71.8m points)

macos - CVDisplayLink with Swift

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 h

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...