Vulkan + Swift: Linking the Vulkan SDK using Swift Package Manager

On the news that Vulkan was finally being made freely available on Apple platforms through the open source release of MoltenVK I jumped at the chance to try out this next-generation graphics framework to see if it will really be the one API to rule them all. Swift is my language of choice for most personal projects, so I thought why not see what it's like integrating Vulkan in a swift project.

But then I started to look through the installation instructions in the VulkanSDK documentation:

Open up the "Link Binary With Libraries" section. Drag the Vulkan Framework from your application's top directory into the list area

... oh ...

Open up the Scheme panel for your project Add the environment variable VK_LAYER_PATH
Set it to vulkansdk/macOS/etc/vulkan/explicit_layers.d

... I don't like where this is going ...

you should see that the errors have cleared out.

If not, follow whatever procedures are necessary for adding a private framework in your version of Xcode. These procedures may vary across versions of Xcode.

"Whatever procedures" is not a phrase I like to see in software documentation. I like to manage my builds through explicit, consistant, repeatable configurations and scripts. Here they expect me to set up the build configuration through a GUI like some kind of pleb.

So instead I set up Vulkan as a Swift Package. Here's how it works, step by step:

1. Download and Setup the LunarG Vulkan SDK

So the first step is to get the Vulkan SDK from LunarG, which can be downloaded as a tar archive.

It's possible to use Vulkan using just the MoltenVK framework, but the SDK offers some additional utility, for instance ready-to-use validation layers. Vulkan doesn't come with any validation or debug messages built-in for performance reasons, so this is nice to have.

Once the SDK is downloaded, unpack it into a suitable location. For instance, I put mine in a directory called ${Home}/Projects/SDKs.

After the SDK is unarchived, follow the instructions in the docs to finish setting it up. You'll need to modify your PATH and add some additional environment variables to make the SDK components available.

From here on out, I will be referring to the absolute path of the macOS directory of the installed SDK as: $MY_SDK_ROOT. So given where I placed my SDK, this is defined on my system like so:

MY_SDK_ROOT=${Home}/Projects/SDKs/LunarG/vulkansdk-macos-1.0.69.0/macOS

2. Create a Package-Config Entry

The LunarG SDK is designed to work with the XCode build system, which is a huge bummer because it's GUI driven. We want to get Vulkan to work with SPM, which is a command-line and configuration driven tool like an adult would want to use.

The tool we can use to make the Vulkan SDK play nice with SPM is pkg-config. This lets us create a configuration file to let the compiler know where to find the Vulkan dynamic library and headers.

So first we create a file called vulkan.pc, and make it look like this:

prefix=$MY_SDK_ROOT
libdir=${prefix}/lib
includedir=${prefix}/include

Name: vulkan
Description: Vulkan SDK
Version: 1.0.69
Libs: -L${libdir} -lvulkan
Cflags: -I${includedir}

Essentially this is telling clang that we want to link against libvuklan.dlyb, and it's specifying the location of that dynamic library as well as the headers.

Once the file's been created, we need to tell pkg-config to look for it. We do so by adding the path to this file to the PKG_CONFIG_PATH variable like so:

PKG_CONFIG_PATH=/path/to/vulkan.pc: $PKG_CONFIG_PATH

To see that it worked, run the following:

$ pkg-config --libs vulkan

And the output should be the flags specified in the .pc file:

-L<MY_SDK_ROOT>/lib -lvulkan

3. Create a System Module Package for Vulkan

Now that our framework is in the right place, we need to make it available as a Swift System Module.

First we need to create a swift package of type system module:

$ swift package init --type system-module Cvulkan

Next we edit the Package.swift file we just created to reference the pkg-config entry we created in the previous step:

import PackageDescription

let package = Package(
    name: "CVulkan",
    pkgConfig: "vulkan",
    dependencies: [
    ]
)

Next we can create a header file to include the vulkan headers:

$ cd Cvulkan & touch shim.h

And we edit Cvulkan/shim.h to include the vulkan header:

// shim.h
#import </vulkan/vulkan.h>

Now that our shim is set up, we can edit the module.modulemap file generated by swift package manager to include our shim, and link to the public framework. It should look like this:

module Cvulkan [system] {
    header "shim.h"
    link "vulkan"
    export *
}

Finally, we need to make our module into a git repo so it can be referenced by other packages. From inside the CMoltenVK directory:

$ git init
$ git add .
$ git commit -am "initial commit"
$ git tag 0.1.0

4. Include MoltenVK in a Swift Executable

Now that everything is set up, we can create a swift module for our executable which will use MoltenVK and Vulkan. First we create the package:

$ swift package init --type executable VulkanApp

Now let's edit the package to include the CMoltenVK package. The dependancies should look like this:

let package = Package(
    name: "VulcanApp",
    dependencies: [
        .package(url: "../CVulkan", .branch("master")),
    ],
    ...
)

Here ../CVulkan is the relative path to the CMoltenVK git repo on our local system. In this case we're assuming the two swift modules are in the same parent directory.

Now that our package is set up, let's add some vulkan code to our project, and try to build it. Let's open Sources/VulkanApp/main.swift and add some Vulkan code. It should look like this:

// main.swift
import CVulkan

var instance: VkInstance?
var createInfo = VkInstanceCreateInfo()
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
let result = vkCreateInstance(&createInfo, nil, &instance)
print("created instance result: \(result)")
vkDestroyInstance(instance, nil)

And finally let's build and run to see if it worked. If we run the following commands:

$ swift build
$ ./.build/x86_64-apple-macosx10.10/debug/Vulcan

We should see the expected output:

created instance result: VkResult(rawValue: 0)

A result value of zero means the instance was created successfully.

And that's it! We've got Vulkan integrated in a swift application using Swift Package Manager.

Happy Coding.


Let's work together! I'm a freelance engineer, and I specialize in mobile applications, computer graphics, and image processing. I'm always looking for interesting projects.

[email protected]