The Shape of Everything
A website mostly about Mac stuff, written by August "Gus" Mueller
» Acorn
» Retrobatch
» Mastodon
» Micro.blog
» Instagram
» Github
» Maybe Pizza?
» Archives
» Feed
» Micro feed
January 3, 2018

In Part II I mentioned how drawing through MTKView wasn't happening as frequently as I was asking it, and in Part IV I mentioned that if I specifically call the -draw method of MTKView when needing frequent updates, things ran a lot smoother.

I wasn't happy with that fix because it needed to be sprinkled around my code base whenever there were operations that caused a high load on the system. And what's fine on my machine might not be fine on a six year old machine. But OK, I'll do it because it obviously needs to be done. But I'm not happy about it, because it just felt wrong.

A few days ago I found myself needing to subclass MTKView (for a reason I'll get to in a moment), and while exploring its headers I noticed it implemented CALayerDelegate, which had me wonder I could move my -draw call into one of the delegate methods? It seemed like -displayLayer: was the obvious choice, so I implemented that in my subclass, called the super method and [self draw] immediately after, and everything began running perfectly smooth. Hurray!

I still don't understand why MTKView isn't doing this automatically for me, but my solution seems to be working out everywhere so I'll probably stop looking for reasons.

There's two ways to draw with MTKView. The first is to set a delegate of the class which implements -drawInMTKView:, or you can subclass MTKView and do your drawing in -drawRect:. I originally chose the delegate approach since I already had the drawing operations set up in another class.

Experienced Cocoa devs will notice that the -drawInMTKView: only passes in a view to draw to, and not an update rect of the region that needs to be changed. I thought that was a bit odd at first, but once you understand the architecture of how Metal displays pixels you'll soon realize that you're not drawing to the same texture on every pass like you would with regular NSView subclasses or even OpenGL. Instead, there's usually three textures that are used for getting pixels up to the display. One for your app to draw to, one for the GPU, and then one for the display. It's covered pretty well (with this timestamp link) in What's New in Metal, Part 2 from WWDC 2015.

And even if you subclass MTKView and implement -drawRect: which passes a region to be updated, you'll soon discover that the region handed to you is the entirety of the view. Which makes the CGRect part of that method call kind of pointless.

I get it though, you've got three different textures that need to be managed and it'll need to remember which region was updated on the previous draws and the textures aren't always given in the same order. And those textures come and go as your view resizes or maybe just because of other reasons. So MTKView throws it's hands up and says "Draw everything, all the time".

I still think that's suboptimal though, and I'm not afraid of managing those updates, so I wrote a (surprisingly small) bit of code to do just that and I ended up seeing this when I turned off display sync (via CAMetalLayer):

That's the Quartz Debug app showing a peak of 205 frames per second worth of drawing in Acorn. That was on a Late 2015 iMac, with a specially crafted brush, but wowsers anyway.

Previously: Part I, Part II, Part III, Part IV, Part V.