Setting up the Project (Metal Part 1)
Following the amazing tutorial series by Rick Twohy, creating a basic macos app project in xcode using Swift and Storyboards. Utilizing the Metal Kit library to access the Metal APIs to render 3D graphics, initializing the basic components to setup the render pipeline and start outputing 3D graphics on the screen.
Source Code
References
Table of Content
Creating the XCode project
We just need to create a very basic macOS app using Swift and Storyboards for the UI.
MTKView
Create a new Cocoa Class file, extending from MTKView, which in turn extends NSView.
This will be connected to your main Storyboard for the Game View, where we will output the rendering of the pipeline.
The Command Structure
Command Structure Images from Apple Docs
The Metal Graphics API uses a Command Structure to handle all the petitions from the CPU to render graphics in the GPU.
Render Pipeline Descriptor Image Source
The Command Buffers contain the instructions the the CPU needs to execute.
The Command Queue holds all the Command Buffers and ensures they execute timely and in order. It also handles executions and results coming to/from Compute Shaders.
Resources
-
- MTL Render Pipeline Descriptor
- Color Attachments
- Pixel Format
- Vertex/Fragment Functions
- MTL Library
- MTL Function Type
- Vertex/Fragment/Kernel
- MTL Function
- .metal files
- MTL Function Type
- MTL Library
- Color Attachments
- MTL Render Pipeline Descriptor
Basic Render Pipeline
- The Device represents the GPU device in the machine.
- From the Device, we create the Commmand Queue.
- We create Command Buffers using the Command Queue.
- The Render Command Encoder is created out of the Command Buffer (There are many types of Command Encoders, Render is for Graphics Rendering, Compute would be for Computations, as in Compute Shaders)
- We use the Render Pass Descriptor for this, which includes information about the output buffers to show the result of the rendering.
- At one point we will set the Render Pipeline State to the Render Command Encoder.
- To create the Render Pipeline State, we first need to create the Render Pipeline Descriptor.
- For creating the Render Pipeline Descriptor, we need to create a Library first, which will let us create the Functions for Vertex and Fragment calculations.
- Once we have the Library and the Functions, we can create the Render Pipeline Descriptor.
- With the Render Pipeline Descriptor, we can tell the Device to create the Render Pipeline State.
- Now we can set the Render Pipeline State to the Render Command Encoder.
- And we can tell the Command Buffer to endEncoding(), present() to the drawable, and commit() to schedule its execution.
Code
Shaders
For defining the Shaders, we need to create a Metal file.
1#include <metal_stdlib> 2using namespace metal; 3 4vertex float4 basic_vertex_shader(){ 5 return float4(1); 6} 7 8fragment half4 basic_fragment_shader(){ 9 return half4(1); 10}
Game View
For now this will just clear the screen with a basic green color, every frame.
1import MetalKit 2 3class GameView: MTKView { 4 5 var commandQueue: MTLCommandQueue! 6 var renderPipelineState: MTLRenderPipelineState! 7 8 required init(coder: NSCoder) { 9 super.init(coder: coder) 10 11 // device is an abstract representation of the GPU 12 // allows to create Metal GPU objects and send them down to the GPU 13 self.device = MTLCreateSystemDefaultDevice() 14 15 // clearColor fills the screen each time the GPU clears the frame (60 times per second at 60 fps) 16 // rgba is 0-1 17 self.clearColor = MTLClearColor(red: 0.43, green: 0.73, blue: 0.35, alpha: 1.0) 18 19 // how pixels are stored 20 self.colorPixelFormat = MTLPixelFormat.bgra8Unorm 21 22 // create the command queue to handle commands for the GPU 23 self.commandQueue = device?.makeCommandQueue() 24 25 createRenderPipelineState() 26 } 27 28 func createRenderPipelineState(){ 29 30 let library = device?.makeDefaultLibrary() 31 32 // at compile time it will pick the right vertex and shader functions by matching the names 33 let vertexFunction = library?.makeFunction(name: "basic_vertex_shader") 34 let fragmentFunction = library?.makeFunction(name: "basic_fragment_shader") 35 36 // create the descriptor for the render pipeline, make the pixel format match the device 37 let renderPipelineDescriptor = MTLRenderPipelineDescriptor() 38 renderPipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unorm 39 40 // set the vertex and fragment functions 41 renderPipelineDescriptor.vertexFunction = vertexFunction 42 renderPipelineDescriptor.fragmentFunction = fragmentFunction 43 44 // create the render pipeline state using the render pipeline descriptor 45 do { 46 renderPipelineState = try device?.makeRenderPipelineState(descriptor: renderPipelineDescriptor) 47 } catch let error as NSError { 48 print(error) 49 } 50 } 51 52 override func draw(_ dirtyRect: NSRect) { 53 54 // get references if available, else return 55 guard let drawable = self.currentDrawable, let renderPassDescriptor = self.currentRenderPassDescriptor else { return } 56 57 // cretae a command buffer 58 let commandBuffer = commandQueue.makeCommandBuffer() 59 60 // create the render command encoder 61 // pass the render pass descriptor, which includes pixel information and destination buffers 62 let renderCommandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) 63 64 // set the render pipeline state to the render command encoder 65 renderCommandEncoder?.setRenderPipelineState(self.renderPipelineState) 66 67 // TODO: send info to render command encoder 68 69 // after passing all the data 70 renderCommandEncoder?.endEncoding() 71 72 // the command buffer will present the result of the rendering when it's done 73 commandBuffer?.present(drawable) 74 75 // execute the command buffer 76 commandBuffer?.commit() 77 } 78}