Image for post
Image for post

Apple Metal is Apple’s primary computer graphics API, and after depreciating OpenGL, the only graphics API supported and maintained by Apple.

Image for post
Image for post
  • 📱 iOS / iPad OS
  • Objective C++
  • Swift

Setup

First install:

# 🐑 Clone the repo
git clone https://github.com/alaingalvan/metal-seed --recurse-submodules
# 💿 go inside the folder
cd metal-seed
# 👯 If you forget to `recurse-submodules` you can always run:
git submodule update --init
# 👷 Make a build folder
mkdir build
cd build
# 🖼️ To build your Visual Studio solution on Windows x64
cmake .. -A x64
# 🍎 To build your XCode project on Mac OS
cmake .. -G Xcode
# 🍎📱 To build your XCode project targeting iOS / iPad OS
cmake .. -G Xcode -DCMAKE_SYSTEM_NAME=iOS
# 🐧 To build your .make file on Linux
cmake ..
# 🔨 Build on any platform:
cmake --build .

Overview

The following will explain snippets that can be found in the Github repo, with certain parts omitted, and member variables (mMemberVariable) declared inline without the m prefix so their type is easier to see and the examples here can work on their own.

Window Creation

Image for post
Image for post
#include "CrossWindow/CrossWindow.h"
#include "Renderer.h"
#include <iostream>void xmain(int argc, const char** argv)
{
// 🖼 Create Window
xwin::WindowDesc wdesc;
wdesc.title = "Metal Seed";
wdesc.name = "MainWindow";
wdesc.visible = true;
wdesc.width = 640;
wdesc.height = 640;
wdesc.fullscreen = false;
xwin::Window window;
xwin::EventQueue eventQueue;
if (!window.create(wdesc, eventQueue))
{ return; };
// 🌋 Create a renderer
Renderer renderer(window);
// 🏁 Engine loop
bool isRunning = true;
while (isRunning)
{
bool shouldRender = true;
// ♻️ Update the event queue
eventQueue.update();
// 🎈 Iterate through that queue:
while (!eventQueue.empty())
{
//Update Events
const xwin::Event& event = eventQueue.front();
// 💗 On Resize:
if (event.type == xwin::EventType::Resize)
{
const xwin::ResizeData data = event.data.resize;
renderer.resize(data.width, data.height);
shouldRender = false;
}
// ❌ On Close:
if (event.type == xwin::EventType::Close)
{
window.close();
shouldRender = false;
isRunning = false;
}
eventQueue.pop();
}
// ✨ Update Visuals
if (shouldRender)
{
renderer.render();
}
}
}

Initialize API

Metal Layer

Every Apple Window can have layers attached to it that handle things like OpenGL or Metal.

// ☕ Use CrossWindow-Graphics to create Metal Layer
xgfx::createMetalLayer(&window);
xwin::WindowDelegate& del = window.getDelegate();
CAMetalLayer* layer = (CAMetalLayer*)del.layer;

Device

Image for post
Image for post
// 👋 Declare handles
MTLCommandBuffer* device;
// 🎮 Create device
layer.device = MTLCreateSystemDefaultDevice();
device = layer.device;

Command Queue

Image for post
Image for post
// 👋 Declare handles
MTLCommandQueue* commandQueue;
// 📦 Create the command queue
commandQueue = [device newCommandQueue];

Initialize Resources

Vertex Buffer

Image for post
Image for post
// 📈 Describe Position Vertex Buffer Data
float positions[3*3] = { 1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
0.0f, -1.0f, 0.0f };
// 🎨 Describe Color Vertex Buffer Data
float colors[3*3] = { 1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f };
// 👋 Declare handles
MTLBuffer* positionBuffer;
MTLBuffer* colorBuffer;
// ⚪ Create VBO
positionBuffer = [device newBufferWithLength:sizeof(Vertex) * 3
options:MTLResourceOptionCPUCacheModeDefault];
// 💬 Label VBO
[positionBuffer setLabel:@"PositionBuffer"];
// 💾 Push data to VBO
memcpy(positionBuffer.contents, positions, sizeof(float) * 3 * 3);
// ⚪ Create VBO
colorBuffer = [device newBufferWithLength:sizeof(Vertex) * 3
options:MTLResourceOptionCPUCacheModeDefault];
// 💬 Label VBO
[colorBuffer setLabel:@"ColorBuffer"];
// 💾 Push data to VBO
memcpy(colorBuffer.contents, colors, sizeof(float) * 3 * 3);

Index Buffer

Image for post
Image for post
// 🗄️ Describe Index Buffer Data
unsigned indexBufferData[3] = { 0, 1, 2 };
// ✋ Declare Index Buffer Handle
MTLBuffer* indexBuffer;
// 🃏 Index Data
indexBuffer = [device newBufferWithLength:sizeof(unsigned) * 3
options:MTLResourceOptionCPUCacheModeDefault];
[indexBuffer setLabel:@"IBO"];
memcpy(indexBuffer.contents, indexBufferData, sizeof(unsigned) * 3);

Uniform Buffer

Image for post
Image for post
// 👋 Declare handles
MTLBuffer* uniformBuffer;
// 🗄️ Describe Uniform Data
struct UniformData
{
mat4 projectionMatrix;
mat4 modelMatrix;
mat4 viewMatrix;
} uboVS;
// 🎛️ Create Uniform Buffer
uniformBuffer = [device newBufferWithLength:(sizeof(UniformData) + 255) & ~255
options:MTLResourceOptionCPUCacheModeDefault];
[uniformBuffer setLabel:@"UBO"];
// Update Uniforms...

Shader Libraries

Image for post
Image for post
// Load all the shader files with a .msl file extension in the projectNSError* err = nil;// 📂 Load shader files, add null terminator to the end.
std::vector<char> vertSource = readFile("triangle.vert.msl");
vertSource.emplace_back(0);
std::vector<char> fragSource = readFile("triangle.frag.msl");
fragSource.emplace_back(0);
NSString* vertPath = [NSString stringWithCString:vertSource.data() encoding:[NSString defaultCStringEncoding]];
MTLLibrary* vertLibrary = [device newLibraryWithSource:vertPath options:nil error:&err];
[vertPath dealloc];
NSString* fragPath = [NSString stringWithCString:fragSource.data() encoding:[NSString defaultCStringEncoding]];
MTLLibrary* fragLibrary = [device newLibraryWithSource:fragPath options:nil error:&err];
[fragPath dealloc];
// Load the vertex function from the library
MTLFunction* vertexFunction = [vertLibrary newFunctionWithName:@"main0"];
// Load the fragment function from the library
MTLFunction* fragmentFunction = [fragLibrary newFunctionWithName:@"main0"];

Pipeline State

Image for post
Image for post
// 👋 Declare handles
MTLRenderPipelineState* pipelineState;
// ⚗️ Graphics Pipeline
MTLRenderPipelineDescriptor* pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.label = @"Simple Pipeline";
pipelineStateDescriptor.vertexFunction = vertexFunction;
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = layer.pixelFormat;
// 🔣 Input Assembly
MTLVertexDescriptor* vertexDesc = [MTLVertexDescriptor vertexDescriptor];
vertexDesc.attributes[0].format = MTLVertexFormatFloat3;
vertexDesc.attributes[0].offset = 0;
vertexDesc.attributes[0].bufferIndex = 0;
vertexDesc.attributes[1].format = MTLVertexFormatFloat3;
vertexDesc.attributes[1].offset = sizeof(float) * 3;
vertexDesc.attributes[1].bufferIndex = 0;
vertexDesc.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
vertexDesc.layouts[0].stride = sizeof(Vertex);
pipelineStateDescriptor.vertexDescriptor = vertexDesc;
NSError* error = nil;
// 🌟 Create Pipeline State Object
pipelineState = [device
newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:&error];
if (!pipelineState)
{
NSLog(@"Failed to created pipeline state, error %@", error);
}

Rendering

Render Pass

Image for post
Image for post
// 👋 Declare Handles
CAMetalLayer* layer;
// 🤵 Build renderPassDescriptor generated from the view's drawable textures
CAMetalDrawable* drawable = layer.nextDrawable;
MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
MTLClearColor clearCol;
clearCol.red = 0.2;
clearCol.green = 0.2;
clearCol.blue = 0.2;
clearCol.alpha = 1.0;
renderPassDescriptor.colorAttachments[0].clearColor = clearCol;

Command Buffer

Image for post
Image for post
// 👋 Declare Metal Handle
MTLCommandBuffer* commandBuffer;
unsigned viewportSize[2];
if (commandBuffer != nil)
{ [commandBuffer release]; }
mCommandBuffer = [(commandQueue commandBuffer];
(commandBuffer).label = @"MyCommand";
if(renderPassDescriptor != nil)
{
// Create a render command encoder so we can render into something
id<MTLRenderCommandEncoder> renderEncoder =
[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
renderEncoder.label = @"MyRenderEncoder";
// Set the region of the drawable to which we'll draw.
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, static_cast<float>(viewportSize[0]), static_cast<float>(viewportSize[1]), 0.1, 1000.0 }];
[renderEncoder setRenderPipelineState: pipelineState]; [renderEncoder setCullMode:MTLCullModeNone]; [renderEncoder setVertexBuffer:positionBuffer offset:0 atIndex:0]; [renderEncoder setVertexBuffer:colorBuffer offset:0 atIndex:1]; [renderEncoder setVertexBuffer:uniformBuffer offset:0 atIndex:2]; [renderEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:3 indexType:MTLIndexTypeUInt32 indexBuffer:indexBuffer indexBufferOffset:0]; [renderEncoder endEncoding]; [commandBuffer presentDrawable:drawable]; [commandBuffer commit];
}

Destroying Handles

Like most system level programming languages with managed memory models, in Objective C++ you must destroy any objects that you create. This however isn’t the case if automatic reference counting (or ARC) is enabled at some point in your code’s execution with @autoreleasepool.

void destroyAPI()
{
if (commandBuffer != nil)
{
[commandBuffer release];
}

[commandQueue release];

[device release];
}
void destroyResources()
{
[fragmentFunction release];
[vertexFunction release];

[vertLibrary release];
[fragLibrary release];
[positionBuffer release];
[colorBuffer release];
[indexBuffer release];
[uniformBuffer release];

[pipelineState release];
}

Conclusion

Metal is arguably the easiest modern computer graphics API to use, with smart defaults and an intuitive API that maps easily to modern GPUs.

Written by

https://Alain.xyz | Graphics Software Engineer @ Marmoset.co. Guest lecturer talking about 🛆 Computer Graphics, ✍ tech author.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store