Home | Series | Next Article (Metal GPU Programming 02 - Shading a Triangle)

Metal GPU Programming 01 - Clear Screen

Published 2018-07-09

Welcome to an exploration of Metal GPU programming for iOS and macOS. Introduced at WWDC 20141, Metal is a powerful, high performance, low overhead graphics API for both iOS and macOS. We'll build Metal apps ranging from the simplest triangle rasterizer to hybrid raytracers.

Metal resides in the family of low-level graphics APIs2 alongside Vulkan and Microsoft's DirectX 12. Though Metal is low-level, it is largely influenced by the DirectX 11 API. Many of Metal's rasterization state structures mimic DirectX 11's, even considering Apple favored OpenGL until the late 2000's. Nowadays, OpenGL is no longer considered a relevant API on Apple platforms. At WWDC 2018 Apple announced the deprecation of OpenGL, crowning Metal as the de facto graphics API on Apple platforms.

Metal's native interface, Objective-C, is used throughout the examples. The source is C++11 with a small amount of Objective-C, all inside a single Objective-C++ file. No headers or extra libraries are necessary to build the examples, and on macOS you can build and execute directly from the terminal. To manage certificates and build chains we maintain an Xcode project for iOS.

Full source for each tutorial is on gitlab and linked at the end of each article.

Clean Slate

Metal01-Clear running on macOS and iOS

Let's begin with the simplest Metal app: a screen clear. Starting with a simple app allows us to cover UI boilerplate in one tutorial and stands on its own as a clean slate for experimentation.

As of Xcode 9 iOS Simulator 11.0, simulated Metal is not supported and requires a physical device. Since this series covers both macOS and iOS, the examples will still run on a Mac. This is fortunate as getting applications onto iOS devices requires an Apple developer account or rooted device.

This series is a bit different from others. We use Objective-C++, focus on both macOS and iOS, use the lowest-level tools available, and forgo Storyboards and XIBs. On the last point, there's little information on programmatically constructing interfaces with UIKit and Cocoa using Objective-C++. Many apps don't require a native UI. For example, we rarely need storyboards when building scientific visualization or gaming applications. Storyboards add extra overhead and make learning Metal more difficult. We build the interface and all supporting UI programmatically.

Automatic reference counting (ARC) is not used. Memory management is handled explicitly and can be debugged through instruments. Some links for brushing up on Objective-C memory management are given in the references.

Familiarity with C++ and Objective-C is assumed. UIKit and Cocoa code description is minimal as we focus solely on Metal.

Project Set Up


The macOS build is an 8 line makefile:

TARGET      = ex
CXXFLAGS += -O0 -g -std=c++1z
LFLAGS   += -framework Cocoa -framework Metal -framework MetalKit\
            -framework QuartzCore
$(TARGET): main.mm
    $(CXX) $(CXXFLAGS) main.mm $(LFLAGS) -o $(TARGET)
    -$(RM) -rf $(TARGET).dSYM/ $(TARGET)

To build, simply type make into a terminal at the root of the project.

This Makefile is used for all tutorials. Note the lack of CPU optimization (-O0) and addition of debug symbols (-g). We prefer no optimization and symbols as we focus on the GPU and want to debug the CPU easily using lldb. That said, all tutorials work with CPU optimization (-O2).

Also note that we have disabled errors inside the clean target. We don't want make clean to generate errors if clean targets are not present. We could make the recipe line silent though that has no bearing on error output.

For a well-rounded introduction to Makefiles, I recommend Stallman and McGrath's GNU Make A Program for Directed Compilation.


For iOS we use Xcode projects. Xcode allows easy interaction with devices and certificates. Here's a rundown of setting up a minimal iOS Xcode project.

Creating an iOS Project from Scratch

Start Xcode and you should see the standard Welcome to Xcode screen.

Remove all the source files that created (*.h, *.m, *.metal) and delete the storyboard files (*.storyboard). Keep Assets.xcassets as that is where we store launch images.

Delete Launch screen interface file base name and Main storyboard file base name from info.plist. Neither apply for programmatically generated UI.

Check 'Requires full screen' under Project Properties, General, Deployment Info. Project properties are found by clicking on your project in the 'Navigator' on the left-hand side of Xcode.

In the same properties screen under General, App Icons and Launch Images, populate Launch Images Source. Use the Assets directory associated with Assets.xcassets. If you wish, use LaunchImage Generator to generate placeholder screens. If Launch Images Source is not set you will notice visible bars at the top and bottom of your device's screen (pre-iOS 5)3. As of this writing the linked LaunchImage generator doesn't generate iphone X images. To do so, create 1125x2436 (portrait) and 2436x1125 (landscape) images4 and add them as iPhone X LaunchImages.

In Project Settings under Build Phases, Link Binary with Libraries, link with the frameworks: UIKit.framework, Metal.framework, and QuartzCore.framework. Quartz is required for CAMetalLayer and CADisplayLink.

Finish by adding your source files.

Metal Fundamentals

Let's dive into the source. Both iOS and macOS share the same source modulo a few preprocessor directives (main.mm).

Metal Device and Queue Creation

The first step in any Metal app is creating a Metal device. Metal requires device creation on a specific GPU so we use MTLCreateSystemDefaultDevice to target the default GPU. Care should be taken when utilizing multiple GPUs. For example, use MtlCopyAllDevices if you are using an eGPU with VR5 or plan on using both Discrete and Integrated GPUs in a MacBook.

The following code from our tutorial creates the device and a command queue:

// Global variable declaration.
id<MTLDevice>               g_mtlDevice;
id<MTLCommandQueue>         g_mtlCommandQueue;

// ... implementation ...

int renderInit()
  g_mtlDevice = MTLCreateSystemDefaultDevice();
  if (!g_mtlDevice)
    fprintf(stderr, "System does not support metal.\n");
    return EXIT_FAILURE;

  g_mtlCommandQueue = [g_mtlDevice newCommandQueue];

  return EXIT_SUCCESS;

We assign a new default device to g_mtlDevice. Since we aren't building a multithreaded renderer we also create a command queue and assign it to g_mtlCommandQueue. In future entries we'll significantly expand this renderInit function.

Let's be sure to clean up:

void renderDestroy()
  [g_mtlCommandQueue release];
  g_mtlCommandQueue = nil;

  [g_mtlDevice release];
  g_mtlDevice = nil;

We call release in reverse order of creation. renderDestroy is called from a macOS/iOS specific dealloc method.

Frame Callback

Consistent frame timing is required for smooth scene animation. iOS' CADisplayLink and macOS' CVDisplayLink were built to synchronize rendering to the refresh rate of the display. Refresh rate synchronization is important and we'll come back to it in a different series. To synchronize with the display on Apple devices, we add DisplayLink callbacks to the main runloop.

// iOS display link callback.
- (void)displayLinkDidFire:(CADisplayLink *)displayLink

// ...

// macOS display link callback
static CVReturn displayLinkCallback(
    CVDisplayLinkRef displayLink,
    const CVTimeStamp* now,
    const CVTimeStamp* outputTime,
    CVOptionFlags flagsIn,
    CVOptionFlags* flagsOut,
    void* displayLinkContext)
  return kCVReturnSuccess;

Details for registering these callbacks are covered in the source. For iOS search for addToRunLoop and macOS CVDisplayLinkSetOutputCallback.


With a periodic frame callback in hand, lets get some content on the screen. When the display link callback is issued we call the following doRender function:

void doRender()
  if (!g_nsView.metalLayer)
    fprintf(stderr, "Warning: No metal layer, skipping render.\n");

  id<CAMetalDrawable> drawable = [g_nsView.metalLayer nextDrawable];
  id<MTLTexture> texture = drawable.texture;

  // Assumes consistent 60Hz refresh rate. Not a great assumption.
  // We will use mach_absolute_time for animation is later examples.
  static float timeSeconds = 0.0;
  timeSeconds += 0.0166;

  MTLRenderPassDescriptor* passDescriptor =
      [MTLRenderPassDescriptor renderPassDescriptor];
  passDescriptor.colorAttachments[0].texture     = texture;
  passDescriptor.colorAttachments[0].loadAction  = MTLLoadActionClear;
  passDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
  passDescriptor.colorAttachments[0].clearColor =
      MTLClearColorMake(fmod(timeSeconds * 20.0,1.0), 0.3f, 0.3f, 1.0f);

  id<MTLCommandBuffer> commandBuffer = [g_mtlCommandQueue commandBuffer];

  id<MTLRenderCommandEncoder> commandEncoder =
      [commandBuffer renderCommandEncoderWithDescriptor:passDescriptor];
  [commandEncoder endEncoding];

  [commandBuffer presentDrawable:drawable];
  [commandBuffer commit];

The first order of business is to obtain our backbuffer. We retrieve it from CAMetalLayer by calling nextDrawable and fail fast if CAMetalLayer has not been created. From the CAMetalDrawable returned from nextDrawable we retrieve a Metal render target by accessing the texture property.

Now we create a MTLRenderPassDescriptor to attach the render target to a rasterization command encoder. While we only specify one color attachment, any reasonable number of color, depth, or stencil attachments are usable. More on this in a later tutorial.

We populate colorAttachment zero with the backbuffer texture that we obtained from CAMetalLayer. Alongside setting the loadAction to MTLLoadActionClear, this tells Metal which texture to clear on load. Normally this color attachment would be written from a shader, but a fixed-function clear is directly supported by Metal. Shaders are introduced in the next tutorial.

The clear color is set using the clearColor property. We animate the clear color in order to tell that the scene is properly rendered beyond the first frame.

Almost finished. Lets create a MTLCommandBuffer to embed our render pass inside. Create one by calling commandBuffer on the global metal queue g_mtlCommandQueue that we created in renderInit. Then call the command buffer's renderCommandEncoderWithDescriptor which begins encoding our render pass. Since we are only using a fixed-function clear, there's nothing to encode in the pass and we immediately call endEncoding.

The last two lines schedule the drawable's present and commit the command buffer to the GPU. Whew! All done.

iPhone Development Hint

iPhone device logs where instrumental in debugging OS-level UIKit issues, so they deserve special mention. To access the iPhone device logs go to Window -> Devices and Simulators in Xcode. The logs have saved me more than once when the App crashes in system libraries.


We covered a handful of Metal classes. Here's an outline of all metal usage in this tutorial with links to documentation covering each class.


This wraps up the bulk of the boilerplate required to get iOS and macOS rendering. Most of the UIKit and Cocoa details are found in the source behind TARGET_OS_IPHONE and TARGET_OS_MAC preprocessor definitions, respectively.

Prior to the announcement of Metal most developers assumed Vulkan was going to be the low-level cross-platform API. In hindsight, Metal appears to have been the right step for the industry. Metal strips away much of the verbosity of other APIs and maintains much of the power.


Feel free to comment or ask questions on Steem.


Gitlab Repository containing the complete source for this example.


Articles In Series

  1. Apple's Metal announcement 

  2. Metal overview

  3. Extra discussion regarding launch images. 

  4. Screen resolution for each iOS device is listed in Apple's human interface guidelines 

  5. Apple discussed using multiple GPUs with VR at WWDC 2018. Skip to 22 minutes.