Metal-cpp Tutorial 1: Creating a Window
Before we begin…
In this tutorial, we look at how to create a window and fill it with a solid color. But, before we can jump right into creating a window and using the Metal Graphics library. It is important we take a step back and look at how a metal application is structured and executed in C++.
Every macOS application that is written uses a single instance of a NSApplication object. The purpose of this object is to: provide a main event loop, distribute/receive events to appropriate objects, manage extra windows and menus, and setting up autorelease pools (a safety net that ensures us that objects that have been “de-allocated” are removed from memory). A NSApplication object will also have a delegate that is notified when the application starts, terminates, hidden or active. A delegate is an object that we pass to an NSApplication that allows us to customize behavior of the application at a specific point. Using this delegate we can customize our window and menu setup.
It is also important to note that Metal-cpp follows the object allocation policies of Cocoa and Cocoa Touch. With that being said, the memory management model of Metal objects are based on object ownership. If you ever worked with Rust (#TeamC++), then the concept of ownership should be familiar to you (not exactly tho). Metal-cpp objects are reference counted and are allowed to have more than one owner. However, if an object has no owners, then the runtime system destroys the object automatically.
Setting Up The Application Environment
As said earlier, every macOS application uses a single instance of NSApplication to help facilitate the flow of the application.
A typical main file could look like this:
A lot of things are happening in these few lines of code, so lets break it down. In line 3 we initialize an AutoreleasePool object so that future calls to “NS::AutoreleasePool::alloc()->init()” will return the instance created at line 3. We use this pool object to allocate metal objects that will be used for rendering. In line 6, we create an application delegate object that we can use to customize the behavior of the application at different moments during the initialization, such as when the application has finished launching and is ready for its window to be created. In line 11, we call the “sharedApplication()” static function that will return an instance of a NSApplication application. In lines 16 and 17, we set the delegate object and call the run function to start the main event loop/application loop/game loop. In line 18, we release the AutoreleasePool object to clean up any memory left behind.
Application Delegate
The application delegate is an object that allows us to customize the behavior of application when it receives a notification from the macOS system.
The application delegate object class is inherited from an ApplicationDelegate parent class in the NS namespace. Our application delegate object overrides 3 virtual functions that will be called during the execution of “appNS->run()”. These functions allows us to customize our behavior during the application set up. These functions take in a NS::Notification objectt.
The function “applicationWillFinishLaunching()” is called when the application initialization is about to complete. We can use this function to set up the menu bar (the menu on top of the screen by calling “createMenuBar()”), set the activation policy (which determines how an app may be activated), and set up any other objects before the main event loop executes.
The function “applicationDidFinishLaunching()” is called when the main run loop has started but before any events have been processed. We use this function to set up the window, and any metal related objects.
Looking at “application.cpp”, in line 10, we make a function call to “MTL::CreateSystemDefaultDevice()” which will return a pointer to an instance of a metal “device”. In line 16, we set up a window object passing in the content size (size of the window), styleMask (allows us to set properties of what the window can do/look), type of backing (tells the window how the drawing should be done using the display buffer), and a flag if the window should be created immediately or until its moved. In lines 22–25, we create a MTK::View object that handles the nitty gritty details that allows us draw the content that we want on to the screen using Metal. This MTK::View object allows us to control the behavior of what gets drawn and how. Using the MTK::View object, we can set the format of the pixel type, and we can clear the screen to a preset color. In lines 28 and 29, we create a view delegate object and set that as a delegate of the MTK::View object; more on this next section. In line 32, we set window to display this “view”. In line 33, we set the window title to use. In line 36, we have the option to set the window as front of the screen list. In line 39, we get the NSApplication object from the notification object. Then in line 40, we tell macOS that this application should be activate regardless of any other macOS applications being active.
The function “applicationShouldTerminateAfterLastWindowClosed()” will cause the application to exit out of the main event loop when all the windows are closed.
Before we move on to the next part, lets talk about the “createMenuBar()” function. In this function we set up the menu bar that we can interact with to cause the window to do whatever we want it to do. We can also execute different types of interactions with keyboard presses. In this function we set up two different ways to close the window and exit out of the application. In line 64, we create a “mainMenu” Menu instance that manages the applications menu. Using this Menu object we can add MenuItem objects to it, which will have some kind of command functionality. Using this “mainMenu” object we can add other menu items to it that will in turn have menus of their own. In line 67 and 68, we create another Menu instance and a MenuItem instance. Inside the “init()” function call we pass in a NS::String object (Metal default string object) with the argument of “Appname” which specifies to name this menu title as the application name. In lines 77 to 80, we create a “callback function” instance that we can attach to a MenuItem instance. In line 83, we register that callback to a MenuItem instance along with the name, and the key to be pressed to execute that callback. In line 84, we set the key that should be pressed along with the callback action key. In this case we chose the Command key. Then finally in line 86, we add the MenuItem instance as a sub-menu option for the Menu instance “appMenu”. Whew! Lines 90 to 104, are essentially following the same pattern as above to add another menu titled “Window” with a sub-menu option to close the window. In lines 107 and 108, we set our created MenuItem instances as sub-menus of the main menu! In lines 110 to 114, we release the objects we created since we don't need them anymore. However in line 117 we return “mainMenu->autorelease()” HUH????. Essentially what is going on here is that by calling “autorelease()” we can refer the automatic object destruction to a later time. We do this because the function call return is assigned to a Menu instance in line 45.
The View Delegate
Now that we learned how to set up a window and how we can customize it to add different type of functionality. Its now time we move on to see HOW we can render a MTK::View.
The View class is inherited from the MTK::ViewDelegete class. Our view delegate class overrides the “drawInMTKView()” function that is called every frame (I say frame here but a better choice of words would be application loop) from the main event application loop. MTK::View forwards events to the view delegate class, such as a “render” event. Once “drawInMTKView()” is called, it will take the renderer object created in the constructor and call the draw function on it passing in the MTK::View object.
Finally Rendering
Finally now we can get to talk about rendering something to the window.
The Renderer class purpose is to draw whatever is contained in the MTK::View object. We first construct this object by passing in a MTL::Device object which we can use to get a new Command Queue object that fill with rendering commands via the Command Buffer. Line 14, exactly does this action. When it is time to draw to the screen, the “Draw()” function is called. In line 25, we first have to create AutoreleasePool object to manage Metal objects. In lines 28 to 32, we create a CommandBuffer object that lets us fill the buffer with different types of “commands” that the GPU will execute once we commit it. Then we get the current RenderPassDescriptor object that describes all the textures for the GPU to render and pass that into the RenderCommandEncoder object to specify what to do when drawing starts and ends. In line 34 we stop encoding commands into the command buffer and then in line 36 and 38; we tell the command buffer to draw to the window and then commit all the encoded commands to the GPU. In line 40, we clean up any temporary objects created at the end of the frame, by releasing the pool.
If everything has been done right your application should look like this:
Final thoughts…
That was a lot of work to get through just to draw a solid color to the screen. In the next tutorial, we gonna start drawing TRIANGLES!!!!!!!! Link to project on Github.