Vulkan promises to be an API built to provide access to GPUs.

My current understanding of GPUs is that they can perform highly parallelized operations, perfect for graphics applications.

I repeat, this is all of my knowledge of GPUs at this point of writing. My goal in this post is to send some sort of operation to a GPU and see the result.

How hard could it be?

Baby Steps

Welp, here’s the spec: https://www.khronos.org/registry/vulkan/specs/1.2/html/vkspec.html#preamble.

Over 1206 pages of spec documentation (in pdf form), phew.

I guess the place to start is installing whatever I need to use the Vulkan API, then do something really stupid. Like check if I have a graphics card on my Macbook!

Toolchain

It looks like Vulkan provides an SDK as a dmg on MacOS where I’m developing, so I’ll extract that to ./vulkan_sdk in my local directory.

I’ll write a simple CMakeLists.txt to build my application and link to my downloaded SDK and enable some C++11 goodness.

cmake_minimum_required(VERSION 3.10)

# set the project name
project(VulkanPlaying)

# Project variables, assume a local install of vulkan-sdk
set(VULKAN_SDK_PATH "${CMAKE_CURRENT_SOURCE_DIR}/vulkan-sdk/macOS")

# add the executable
add_executable(playing playing.cpp)

# Explicit error checking.
set(CMAKE_CXX_FLAGS_DEBUG_INIT "-Wall")
set(CMAKE_CXX_FLAGS_RELEASE_INIT "-Wall")

target_include_directories(playing PRIVATE "${VULKAN_SDK_PATH}/include/")
target_link_directories(playing PRIVATE "${VULKAN_SDK_PATH}/lib/")

target_link_libraries(playing PRIVATE vulkan)

set_target_properties(
  playing
  PROPERTIES CXX_STANDARD 11
             CXX_STANDARD_REQUIRED YES
             CXX_EXTENSIONS NO)

So our source code will go in a file called playing.cpp in the source directory and we’ll link the executable to libvulkan in the VulkanSDK path.

Level 1: Get the (physical) hardware

Okay, so first step of the challenge is get access to a physical device in a not-crazy way that will work across more than just my machine.

There’s a function called vkEnumeratePhysicalDevices to see what hardware I’m working with, but it’s a little wonky for someone coming from a higher-level language.

Basically vkEnumeratePhysicalDevices requires that you pass a pointer that has room for all the physical devices your system could have. Vulkan isn’t going to go around allocating memory for arrays, allocate your own memory! But how do we know how big to make the buffer we pass in when we don’t know how many physical devices are on our system? Well the API reuses the same function for that purpose.

First you use the API to get the number of devices (passing nullptr for the other arg), then you allocate, and finally recall the API with space for the devices.

Since we’re cheating with a bit of C++, we’ll just use the vector class to help us out here.

// Get the number of devices.
uint32_t numDevices = 0;
VK_CHECK(vkEnumeratePhysicalDevices(instance, &numDevices, nullptr));
VK_ASSERT(numDevices > 0);

// Now that we know how many devices we have, get this in a vector.
printf("Physical device count: %d\n", numDevices);
std::vector<VkPhysicalDevice> devices;
devices.resize(numDevices);

// Now actually grab the physical device definitions.
VK_CHECK(vkEnumeratePhysicalDevices(instance, &numDevices, devices.data()));

Let’s see what we’re working with on this ol’2018 Macbook Pro.

VK: VK_ERROR_INCOMPATIBLE_DRIVER - vkCreateInstance(&createInfo, nullptr, &instance), /Users/jpfeltracco/repos/vulkan/playing.cpp:29

Oh dear me. This isn’t good. Turns out Vulkan doesn’t have a native MacOS implementation, so it fails to run.

There’s a way around this: https://github.com/KhronosGroup/MoltenVK.

MoltenVK apparently implements the Vulkan API on top of Metal, and it’s shipped with the SDK.

But there’s a little manual config to get it going. Basically you need these two environment variables defined (may want to wrap these in a helper script of sorts).

export VK_ICD_FILENAMES="${VULKAN_SDK_PATH}/macOS/share/vulkan/icd.d/MoltenVK_icd.json"
export VK_LAYER_PATH="${VULKAN_SDK_PATH}/macOS/share/vulkan/explicit_layer.d"

Okay, re-running, what’s the result of some debug printing?

------------------
deviceName: AMD Radeon Pro 560X
apiVersion: 4198554
vendorID: 4098
deviceID: 26607
deviceType: VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
------------------

------------------
deviceName: Intel(R) UHD Graphics 630
apiVersion: 4198554
vendorID: 32902
deviceID: 16027
deviceType: VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
------------------

Cool, I got my money’s worth! I bought a Macbook with an external GPU, so I’m glad to see I’ve got hanging around here. Vulkan is even nice enough to tell us which is the discrete (external) and which is the integrated card. (I wonder how Vulkan knows?).

For now we can have some simple GPU switching, I’d like to always grab my fancier discrete card if available, or if not, fall back to the integrated card.

Something simple like this should work.

for (const auto device : devices) {
  VkPhysicalDeviceProperties properties = {};
  vkGetPhysicalDeviceProperties(device, &properties);

  switch (properties.deviceType) {
    case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
      if (physicalDeviceProperties.deviceID) break;
      physicalDevice = device;
      physicalDeviceProperties = properties;
      break;
    case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
      physicalDevice = device;
      physicalDeviceProperties = properties;
      break;
    default:
      break;
  }
}

Level 2: Get the (logical) hardware

There’s a layer of indirection between you and the physical hardware handle you found in the Vulkan spec. It looks like you’re required to configure a logical device using the physical device through the API vkCreateDevice.

In order to invoke this API, we need to generate a VkDeviceCreateInfo struct, which is filled with device creation flags, device queue creation, layers, and extensions! Oh my!

You know what, this sounds like a bit much for one article. I’ll leave you hanging for part 2 where I’ll stumble through this concepts to hopefully get a logical device up and running.