How, exactly, do I render Metal on a background thread? - swift

This problem is caused by user interface interactions such as showing the titlebar while in fullsreen. That question's answer provides a solution, but not how to implement that solution.
The solution is to render on a background thread. The issue is, the code provided in Apple's is made to cover a lot of content so most of it will extraneous code, so even if I could understand it, it isn't feasible to use Apple's code. And I can't understand it so it just plain isn't an option. How would I make a simple Swift Metal game use a background thread being as concise as possible?
Take this, for example:
class ViewController: NSViewController {
var MetalView: MTKView {
return view as! MTKView
}
var Device: MTLDevice = MTLCreateSystemDefaultDevice()!
override func viewDidLoad() {
super.viewDidLoad()
MetalView.delegate = self
MetalView.device = Device
MetalView.colorPixelFormat = .bgra8Unorm_srgb
Device = MetalView.device
//setup code
}
}
extension ViewController: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
//drawing code
}
}
That is the start of a basic Metal game. What would that code look like, if it were rendering on a background thread?
To fix that bug when showing the titlebar in Metal, I need to render it on a background thread. Well, how do I render it on a background thread?
I've noticed this answer suggests to manually redraw it 60 times a second. Presumably using a loop that is on a background thread? But that seems... not a clean way to fix it. Is there a cleaner way?

The main trick in getting this to work seems to be setting up the CVDisplayLink. This is awkward in Swift, but doable. After some work I was able to modify the "Game" template in Xcode to use a custom view backed by CAMetalLayer instead of MTKView, and a CVDisplayLink to render in the background, as suggested in the sample code you linked — see below.
Edit Oct 22:
The approach mentioned in this thread seems to work just fine: still using an MTKView, but drawing it manually from the display link callback. Specifically I was able to follow these steps:
Create a new macOS Game project in Xcode.
Modify GameViewController to add a CVDisplayLink, similar to below (see this question for more on using CVDisplayLink from Swift). Start the display link in viewWillAppear and stop it in viewWillDisappear.
Set mtkView.isPaused = true in viewDidLoad to disable automatic rendering, and instead explicitly call mtkView.draw() from the display link callback.
The full content of my modified GameViewController.swift is available here.
I didn't review the Renderer class for thread safety, so I can't be sure no more changes are required, but this should get you up and running.
Older implementation with CAMetalLayer instead of MTKView:
This is just a proof of concept and I can't guarantee it's the best way to do everything. You might find these articles helpful too:
I didn't try this idea, but given how much convenience MTKView generally provides over CAMetalLayer, it might be worth giving it a shot:
https://developer.apple.com/forums/thread/89241?answerId=268384022#268384022
Is drawing to an MTKView or CAMetalLayer required to take place on the main thread? and https://developer.apple.com/documentation/quartzcore/cametallayer/1478157-presentswithtransaction
class MyMetalView: NSView {
var displayLink: CVDisplayLink?
var metalLayer: CAMetalLayer!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setupMetalLayer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupMetalLayer()
}
override func makeBackingLayer() -> CALayer {
return CAMetalLayer()
}
func setupMetalLayer() {
wantsLayer = true
metalLayer = layer as! CAMetalLayer?
metalLayer.device = MTLCreateSystemDefaultDevice()!
// ...other configuration of the metalLayer...
}
// handle display link callback at 60fps
static let _outputCallback: CVDisplayLinkOutputCallback = { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, context) -> CVReturn in
// convert opaque context pointer back into a reference to our view
let view = Unmanaged<MyMetalView>.fromOpaque(context!).takeUnretainedValue()
/*** render something into view.metalLayer here! ***/
return kCVReturnSuccess
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
guard CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) == kCVReturnSuccess,
let displayLink = displayLink
else {
fatalError("unable to create display link")
}
// pass a reference to this view as an opaque pointer
guard CVDisplayLinkSetOutputCallback(displayLink, MyMetalView._outputCallback, Unmanaged<MyMetalView>.passUnretained(self).toOpaque()) == kCVReturnSuccess else {
fatalError("unable to configure output callback")
}
guard CVDisplayLinkStart(displayLink) == kCVReturnSuccess else {
fatalError("unable to start display link")
}
}
deinit {
if let displayLink = displayLink {
CVDisplayLinkStop(displayLink)
}
}
}

Related

Rendering NSView in another context

Im looking for a way to have one NSView draw inside another NSView, to make a preview of that view. To be exact Im using VZVirtualMachineView and want to have a preview only in a grid. I would just use VZVirtualMachineView but there is one issue with it - it captures mouse cursor (can hide it) and reacts to clicks and mouse movement over it. And in this case I want to have only the preview with no interaction and no cursor hiding.
So I thought I might try rendering it inside another NSView using vmView.layer?.render(in: context.cgContext), or some other similar mechanism.
The question is how to do this properly? And how to make it refresh with correct time? Should I use something like CADisplayLink? Do I need to add this view first somewhere or can I draw it without adding? Also I would like to not use something like creating image (unless this would be efficient method)
Here is very simple first try, but Im not happy with that code, so
Please suggest some approaches that I should try.
class VMPreview: NSView {
let vmView: VZVirtualMachineView
var timer: Timer?
init(virtualMachine: VZVirtualMachine) {
self.vmView = .init()
self.vmView.virtualMachine = virtualMachine
super.init(frame: .zero)
addSubview(self.vmView)
dr()
}
func dr() {
self.setNeedsDisplay(bounds)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.dr()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var frame: NSRect {
didSet {
self.vmView.frame = bounds.offsetBy(dx: 0, dy: bounds.height)
}
}
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current else { return }
vmView.layer?.render(in: context.cgContext)
self.setNeedsDisplay(bounds)
}
}

Drawing directly in a NSView without using the draw(_ updateRect: NSRect) function

I would like to draw CGImage pictures directly to a View and with the normal method using the draw func I only get 7 pictures in a second on a new Mac Book Pro. So I decided to use the updateLayer func instead. I have defined wantsUpdateLayer = true and my new updateLayer func is called as expected. But then starts my problem. When using the draw func, I get the current CGContext with "NSGraphicsContext.current?.cgContext" but in my updateLayer func the "NSGraphicsContext.current?.cgContext" is nil. So I do not know where to put my CGImage, that it will be displayed on my screen. Also "self.view?.window?.graphicsContext?.cgContext" and "self.window?.graphicsContext?.cgContext" are nil, too. There are no buttons or other elements in this view and in the window of the view, only one picture, filling the complete window. And this picture must change 30 times in a second. Generating the pictures is done by a separate thread and needs about 1 millisecond for a picture. I think that from "outside" the NSView class it is not possible to write the picture but my updateLayer func is inside the class.
Here is what the func looks like actually:
override func updateLayer ()
{
let updateRect: NSRect = NSRect(x: 0.0, y: 0.0, width: 1120.0, height: 768.0)
let context1 = self.view?.window?.graphicsContext?.cgContext
let context2 = self.window?.graphicsContext?.cgContext
let context3 = NSGraphicsContext.current?.cgContext
}
And all three contexts are nil in the time the function is called automatically after I set the needsDisplay flag.
Any ideas where to draw my CGImages?
The updateLayer func is called automatically by the user interface. I do not call it manually. It is called by the view. My problem is where inside this method to put my picture to be shown on the screen. Perhaps I have to add a layer or use a default layer of the view but I do not know how to do this.
Meanwhile I have found the solution with some tipps from a good friend:
override var wantsUpdateLayer: Bool
{
return (true)
}
override func updateLayer ()
{
let cgimage: CGImage? = picture // Here comes the picture
if cgimage != nil
{
let nsimage: NSImage? = NSImage(cgImage: cgimage!, size: NSZeroSize)
if nsimage != nil
{
let desiredScaleFactor: CGFloat? = self.window?.backingScaleFactor
if desiredScaleFactor != nil
{
let actualScaleFactor: CGFloat? = nsimage!.recommendedLayerContentsScale(desiredScaleFactor!)
if actualScaleFactor != nil
{
self.layer!.contents = nsimage!.layerContents(forContentsScale: actualScaleFactor!)
self.layer!.contentsScale = actualScaleFactor!
}
}
}
}
}
This is the way to directly write into the layer. Depending on the format (CGImage or NSImage) you first must convert it. As soon as the func wantsUpdateLayer returns a true, the func updateLayer() is used instead of the func draw(). Thats all.
For all who want to see my "Normal" draw function:
override func draw (_ updateRect: NSRect)
{
let cgimage: CGImage? = picture // Here comes the picture
if cgimage != nil
{
if #available(macOS 10.10, *)
{
NSGraphicsContext.current?.cgContext.draw(cgimage!, in: updateRect)
}
}
else
{
super.draw(updateRect)
}
}
The additional speed is 2 times or more, depending on what hardware you use. On a modern Mac Pro there is only a little bit more speed but on a modern Mac Book Pro you will get 10 times or more speed. This works with Mojave 10.14.6 and Catalina 10.15.6. I did not test it with older macOS versions. The "Normal" draw function works with 10.10.6 to 10.15.6.

How to get size and position of a sceneWindow under mac Catalyst

I have been able to obtain the size of a sceneWindow when I resize it using
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
print("movement trapped \(windowScene.coordinateSpace.bounds)"
}
within the sceneDelegate. But the x,y coordinates are always 0,0 regardless of where I drag the window to. Looking to be able to dictate where the new sceneWindow is located on the mac's screen relative to the "default" sceneWindow.
you can try to convert window frame from windowScene coordinateSpace to UIScreen coordinateSpace
windowFrame = [window convertRect:window.frame toCoordinateSpace:window.windowScene.screen.coordinateSpace];
Well
I didn't find a way to get this done in UIWindow but remember, Catalyst does support AppKit and you can call it anytime use objc runtime.
So here comes up the idea:
Get NSWindows from NSApplication
Get UIWindow from view.window
Compare some magic and lookup our target NSWindow
Get the frame of that NSWindow
var targetNSWindow: AnyObject? = nil
let nsWindows = (NSClassFromString("NSApplication")?.value(forKeyPath: "sharedApplication.windows") as? [AnyObject])!
for nsWindow in nsWindows {
let uiWindows = nsWindow.value(forKeyPath: "uiWindows") as? [UIWindow] ?? []
if uiWindows.contains(view.window!) {
targetNSWindow = nsWindow
}
}
if let found = targetNSWindow {
print(found.value(forKeyPath: "_frame")!)
}
And here is a sample output.
NSRect: {{818, 296}, {964, 614}}
A little bit more, you can have your window information from sceneDelegate and compare the magic with it in a similar way. But be careful, sometimes you don't have any window when the app just loads. Do the jobs in DispatchQueue.main.async block if that happens.

How to control one IB custom view with mouseEntered/-Exited in another IB custom view

I'm sorry for my incompetence, but I'm new to Cocoa, Swift, and object-oriented programming, in general. My primary sources have been Cocoa Programming for OS X (5th ed.), and Apple's jargon- and Objective-C-riddled Developer pages. But I'm here because I haven't seen (or didn't realize that I saw) anything that speaks to this problem.
I want to change the contents of one IB-created custom view, LeftView, by mouseEntered/-Exited actions in another IB-created custom view, RightView. Both are in the same window. I created a toy program to try to figure things out, but to no avail.
Here's the class definition for RightView (which is supposed to change LeftView):
import Cocoa
class RightView: NSView {
override func drawRect(dirtyRect: NSRect) {
// Nothing here, for now.
}
override func viewDidMoveToWindow() {
window?.acceptsMouseMovedEvents = true
let options: NSTrackingAreaOptions =
[.MouseEnteredAndExited, .ActiveAlways, .InVisibleRect]
let trackingArea = NSTrackingArea(rect: NSRect(),
options: options,
owner: self,
userInfo: nil)
addTrackingArea(trackingArea)
}
override func mouseEntered(theEvent: NSEvent) {
Swift.print("Mouse entered!")
LeftView().showStuff(true)
}
override func mouseExited(theEvent: NSEvent) {
Swift.print("Mouse exited!")
LeftView().showStuff(false)
}
}
And here's the class definition for LeftView (which is supposed to be changed by RightView):
import Cocoa
class LeftView: NSView {
var value: Bool = false {
didSet {
needsDisplay = true
Swift.print("didSet happened and needsDisplay was \(needsDisplay)")
}
}
override func mouseUp(theEvent: NSEvent) {
showStuff(true)
}
override func drawRect(dirtyRect: NSRect) {
let backgroundColor = NSColor.blackColor()
backgroundColor.set()
NSBezierPath.fillRect(bounds)
Swift.print("drawRect was called when needsDisplay was \(needsDisplay)")
switch value {
case true: NSColor.greenColor().set()
case false: NSColor.redColor().set()
}
NSBezierPath.fillRect(NSRect(x: 40, y: 40,
width: bounds.width - 80, height: bounds.height - 80))
}
func showStuff(showing: Bool) {
Swift.print("Trying to show? \(showing)")
value = showing
}
}
I'm sure I'm missing something "completely obvious," but I'm a little dense. If you could tell me how to fix the code/xib files, I would very much appreciate it. If you could explain things like when talking to a child, I would be even more appreciative. When I take over the world (I'm not incompetent at everything), I will remember your kindness.
I came up with a workaround that is much simpler than my prior strategy. Instead of having mouseEntered/-Exited actions in one custom view try to control what is displayed in another custom view, I simply put the mouseEntered/-Exited code into the view that I wanted to control, and then I altered the location of rect: in NSTrackingArea.
Before moving code over by for that approach, I had tried changing the owner: in NSTrackingArea to LeftView() and just moving over the mouseEntered/-Exited code. That generated lots of scary error messages (naive newbie talking here), so I gave up on it. It would be nice to know, though, how one can correctly assign an owner other than self.
In any case, any further thoughts or insights would be appreciated.

Custom Activity Indicator with Swift

I am trying to create a custom activity indicator like one of these. There seems to be many approaches to animating the image, so I'm trying to figure out the best approach.
I found this tutorial with Photoshop + Aftereffects animation loop, but as the comment points out, it seems overly complex (and I don't own After Effects).
tldr: how can I take my existing image and animate it as a activity indicator (using either rotating/spinning animation or looping through an animation)
It's been a while since you posted this question, so I'm not sure if you've found the answer you're looking for.
You are right there are many ways to do what you are asking, and I don't think there's a "right way". I would make a UIView object which have a start and a stop method as the only public methods. I would also make it a singleton, so it's a shared object and not possible to put multiple instances on a UIViewController (imagine the mess it could make).
You can add a dataSource protocol which returns a UIViewController so the custom activity indicator could add itself to it's parent.
In the start method I would do whatever animations is needed (transform, rotate, GIF, etc.).
Here's an example:
import UIKit
protocol CustomActivityIndicatorDataSource {
func activityIndicatorParent() -> UIViewController
}
class CustomActivityIndicator: UIView {
private var activityIndicatorImageView: UIImageView
var dataSource: CustomActivityIndicatorDataSource? {
didSet {
self.setupView()
}
}
// Singleton
statice let sharedInstance = CustomActivityIndicator()
// MARK:- Initialiser
private init() {
var frame = CGRectMake(0,0,200,200)
self.activityIndicatorImageView = UIImageView(frame: frame)
self.activityIndicatorImageView.image = UIImage(named: "myImage")
super.init(frame: frame)
self.addSubview(self.activityIndicatorImageView)
self.hidden = true
}
internal required init?(coder aDecoder: NSCoder) {
self.activityIndicatorImageView = UIImageView(frame: CGRectMake(0, 0, 200, 200))
self.activityIndicatorImageView = UIImage(named: "myImage")
super.init(coder: aDecoder)
}
// MARK:- Helper methods
private func setupView() {
if self.dataSource != nil {
self.removeFromSuperview()
self.dataSource!.activityIndicatorParent().addSubview(self)
self.center = self.dataSource!.activityIndicatorParent().center
}
}
// MARK:- Animation methods
/**
Set active to true, and starts the animation
*/
func startAnimation() {
// A custom class which does thread handling. But you can use the dispatch methods.
ThreadController.performBlockOnMainQueue{
self.active = true
self.myAnimation
self.hidden = false
}
}
/**
This will set the 'active' boolean to false.
Remeber to remove the view from the superview manually
*/
func stopAnimation() {
ThreadController.performBlockOnMainQueue{
self.active = false
self.hidden = true
}
}
}
Mind that this is not fully tested and may need some tweaks to work 100%, but this is how I basically would handle it.