I have been dumbfounded for years. I want to maybe write the Linkage program in Swift for the Mac. I absolutely need GPU acceleration to get good performance and that has eluded me for quite some time. I was unable to find any article, until now, that told me how to make my code using Quartz, or Core Graphics, or whatever the F it’s called, to use the GPU for rendering. Apple documentation in this area is complete shit.

I tried various things related to caching an NSBezierPath, setting QuartzGlEnabled in the plist, etc. and none of these things worked. I finally today stumbled on the answer which is surprisingly to set the drawsAsynchronously value of a CALayer (or NSLayer) to true. Doing this one simple thing made my previous test code run at GPU-accelerated speeds. It’s beyond me why every article that talks about GPU acceleration never mentions this setting (except for this stack Overflow answer: https://stackoverflow.com/questions/8715749/performance-when-frequently-drawing-cgpaths).

My test code is thrown together and is rather ugly. All it does is catch mouse button and movement events and then draws a lot of lines. Here’s the code:


@IBDesignable class EditorView: NSView {
//
//  EditorView.swift
//  Linkage
//
//  Created by David Rector on 12/7/18.
//  Copyright © 2018 David Rector. All rights reserved.
//

import Cocoa
import Quartz
//import GLKit

protocol EditorViewDelegate {
	func editorMouseDown()
}

@IBDesignable class EditorView: NSView {
	
	public var delegate: EditorViewDelegate?
	public var document : Document? {
		didSet {
			needsDisplay = true
		}
	}
	
	override func awakeFromNib() {
		super.awakeFromNib()
		
		// NOTE
		// Creating an NSBezierpath here and then filling it's content (moveto, lineto)
		// later can result in a big performance boost. See this article:
		// https://stackoverflow.com/questions/4331835/fast-drawing-using-nsbezierpath-drawing-at-least-4000-segments
		// All moveto and lineto should happen and the stroke called only once.
		
		//self.layer = CALayer()
		
		self.wantsLayer = true
		
		// THIS IS WHAT ENABLE GPU ACCELERATION
		let layer = self.layer
		layer?.drawsAsynchronously = true
		
		//self.layer?.display()
	}
	
	override var isOpaque: Bool {
		get { return true }
	}
	
	private var cache = NSBezierPath()
	
	  override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

		if document == nil {
			return
		}
		
		// Draw the document
		
		let derp = NSString( string: document!.displayName )
		derp.draw(at: NSPoint( x: -2, y: -4 ), withAttributes: [:] )
		
		// Drawing code here.
		let context = NSGraphicsContext.current!.cgContext
		
		context.setFillColor( CGColor( red : 1.0, green: 1.0, blue: 1.0, alpha: 1.0 ) )
		context.fill( dirtyRect )
		
		//context.setMiterLimit( 0 )
		//  context.setLineCap( .round )
		//  context.setLineJoin( .round )
		//context.setShouldAntialias( true )
		
		if startPoint != nil && endPoint != nil {
			for index in 0..<500 {
				context.setStrokeColor( CGColor( red : 0.3, green: 0.9, blue: 1.0, alpha: 1.0 ))
				context.setLineWidth( 1.0 )
				context.move(to: startPoint! )
				context.addLine(to: CGPoint( x: endPoint!.x+CGFloat(index), y: endPoint!.y ) )
				//context.addLine(to: endPoint! )
				//context.addLine(to: CGPoint( x: startPoint!.x, y: endPoint!.y ) )
				//context.addLine(to: startPoint! )
				context.strokePath()
			}
		}

		/*if startPoint != nil && endPoint != nil {
			for index in 0..<500 {
				let path = cache
				path.removeAllPoints()
				path.lineWidth = 1.0
				path.move( to: startPoint! )
				path.line(to: NSMakePoint( endPoint!.x + CGFloat(index * 3), endPoint!.y ) )
				NSColor.black.set()
				path.stroke()
				}
		}*/
    }
	
	var startPoint : NSPoint?
	var endPoint : NSPoint?
	

	override func mouseDown(with event: NSEvent) {
		delegate?.editorMouseDown()
		startPoint = self.convert(event.locationInWindow, from: nil)
		
		//shapeLayer.lineWidth = 0.5
		//shapeLayer.fillColor = NSColor.clear.cgColor
		//shapeLayer.strokeColor = CGColor( red: 0.2, green: 0.8, blue: 1.0, alpha: 1.0)
		//shapeLayer.lineDashPattern = [10,5]
		
		// Good for chains...
		//var dashAnimation = CABasicAnimation()
		//dashAnimation = CABasicAnimation(keyPath: "lineDashPhase")
		//dashAnimation.duration = 0.75
		//dashAnimation.fromValue = 0.0
		//dashAnimation.toValue = 15.0
		//dashAnimation.repeatCount = .infinity
		//shapeLayer.add(dashAnimation, forKey: "linePhase")
		
		
		
		
	}
	
	override func mouseDragged(with event: NSEvent) {
		
		let point : NSPoint = self.convert(event.locationInWindow, from: nil)
		endPoint = point
		
		//let path = CGMutablePath()
		//path.move(to: self.startPoint)
		//path.addLine(to: NSPoint(x: self.startPoint.x, y: point.y))
		//path.addLine(to: point)
		//path.addLine(to: NSPoint(x:point.x,y:self.startPoint.y))
		//path.closeSubpath()
		//self.shapeLayer.path = path
		
		needsDisplay = true
		//self.setNeedsDisplay( CGRect( x:0, y:0, width:1000, height:1000 ) )
	}
	
	override func mouseUp(with event: NSEvent) {
		//self.shapeLayer.removeFromSuperlayer()
		//self.shapeLayer = nil
		endPoint = nil
	}	
}

It’s not pretty but it does work. I can’t even explain why I did some of the things you see here, like create this editor view class; I suspect I did this because I want to use a document-view architecture like I did in the Windows C++ code and this seemed like a good place to start.

Anyhow, the important this is that the layer needs to have that drawsAsynchronously value set to true in order to let the underlying graphics code use the GPU. Maybe this should be the default or maybe Apple should document this somewhere. Finding this after four years of searching should embarrass them. But more likely, it should embarrass me.