Windyland serving my blog

Learning Vulkan - Part1

Recently I am writing some graphics programs and find Vulkan’s API really interesting. So I decide to spend some time on it.

Download the sdk and get it compiled

Let’s start with Vulkan’s sdk, which you can download from lunar

Someone might choose to use linux. It is known that Intel/Nvidia/AMD all have the support for Vulkan in their drivers. So it is quite reasonable especially for Nvidia cards where linux support is great. However since the Vulkan is quite a new thing, I recommand to use windows if you are using an Intel card.

the code below is written in C++

int main() {
  SampleApplication app;
  auto res = app.CreateInstance();
  if (res) {
    printf("error, failed to create instance\n");
    return -1;
  }
  printf("Vulkan instance created\n");
}

As you want to load vulkan’s driver, it is cleaner to only have one “CreateInstance” api for SampleApplication.

In fact, vulkan has its C API designed in the same way. The definition of SampleApplication:

class SampleApplication {
public:
  SampleApplication() {
    app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    app_info.pApplicationName = "Vulkan Example";
    app_info.applicationVersion = 1;
    app_info.pEngineName = "VV";
    app_info.engineVersion = 1;
    app_info.apiVersion = VK_API_VERSION_1_0;
  }
  ~SampleApplication() {
    if (inst) {
      vkDestroyInstance(inst, nullptr);
      inst = nullptr;
    }
  }
  VkResult CreateInstance() {
    assert(!inst);
    VkInstanceCreateInfo inst_info = {};
    inst_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    inst_info.flags = 0;
    inst_info.pApplicationInfo = &app_info;
    inst_info.enabledLayerCount = 0;
    inst_info.ppEnabledLayerNames = nullptr;
    inst_info.enabledExtensionCount = 0;
    inst_info.ppEnabledExtensionNames = nullptr;
    return vkCreateInstance(&inst_info, nullptr, &inst);
  }
private:
  VkApplicationInfo app_info = {};
  VkInstanceCreateInfo inst_info = {};
  VkInstance inst = nullptr;
};

Where Vulkan’s api vkCreateInstance is called to create Vulkan instance in SampleApplication::CreateInstance, and call vkDestroyInstance in its dtor.

The program is completed to find if the system has vulkan support.

So let’s get it compile. You need to include Vulkan’s header from sdk directory

#include <vulkan/vulkan.h>

And get it compile with vulkan loader library named vulkan-1.dll under windows (vulkan.so under linux)

Once you compile this program and run successfully, you will receive “Vulkan instance created” message from console

What’s next

Vulkan has a builtin queue architecture and command buffer pool managed by applications not users.

Before you can send a command such as vkGetLineWidth() to GPU, you need to allocate a command buffer. Before you can allocate a command buffer, you need to setup a command buffer pool. And before you can setup command buffer pool, you need specify the queue.

Those steps are different from OpenGL which will handle command buffer and queue for you. So Vulkan is designed to expose more details about graphic hardware so it is possible to unleash full power of the hardware. Meanwhile it makes Vulkan harder to program with.

What’s we are doing is similar to vulkaninfo program which query vulkan driver’s extensions, every physical devices’s properties, their queues and their own extensions.

  std::vector<VkPhysicalDevice> pdevices;
  res = app.EnumeratePhysicalDevices(&pdevices);
  if (res || pdevices.empty()) {
    printf("no gpu found, res %d\n", res);
    return -1;
  }
  printf("found %llu physical devices\n", pdevices.size());

  // investigate Physical Device
  for (auto i = 0U; i < pdevices.size(); ++i) {
    auto pdevice = pdevices[i];
    VkPhysicalDeviceProperties properties;
    vkGetPhysicalDeviceProperties(pdevice, &properties);
    printf("\t PhysicalDevice%u. %s api %u.%u.%u\n", i, properties.deviceName,
           VK_VERSION_MAJOR(properties.apiVersion),
           VK_VERSION_MINOR(properties.apiVersion),
           VK_VERSION_PATCH(properties.apiVersion));
  }
  printf("using physical device 0\n");

  // investigate Physical Device 0's Queue
  auto pdevice = pdevices[0];
  uint32_t queue_properties_count;
  std::vector<VkQueueFamilyProperties> queue_properties;
  vkGetPhysicalDeviceQueueFamilyProperties(pdevice, &queue_properties_count,
                                           nullptr);
  queue_properties.resize(queue_properties_count);
  vkGetPhysicalDeviceQueueFamilyProperties(pdevice, &queue_properties_count,
                                           queue_properties.data());
  uint32_t queue_family_index = UINT32_MAX;
  for (auto idx = 0U; idx < queue_properties.size(); ++idx) {
    auto queue_property = queue_properties[idx];
    printf("\t QueueIdx%u. ", idx);
    if (queue_property.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
      queue_family_index = idx;
      printf("Graphics ");
    }
    if (queue_property.queueFlags & VK_QUEUE_COMPUTE_BIT)
      printf("Compute ");
    if (queue_property.queueFlags & VK_QUEUE_TRANSFER_BIT)
      printf("Transfer ");
    if (queue_property.queueFlags & VK_QUEUE_SPARSE_BINDING_BIT)
      printf("SparseBinding ");
    printf("has %u depth\n", queue_property.queueCount);
  }

Here is the output in my computer

vulkan instance created
found 1 physical devices
         PhysicalDevice0. GeForce GTX 1080 Ti api 1.0.42
using physical device 0
         QueueIdx0. Graphics Compute Transfer SparseBinding has 16 depth
         QueueIdx1. Transfer has 1 depth
         QueueIdx2. Compute has 8 depth

Create Logical Device

Once you have queue family index, you can specify the physical device and the selected queues to create logical device:

  VkResult CreateDevice(VkPhysicalDevice pdevice, uint32_t queue_family_index) {
    assert(inst && !device);
    VkDeviceQueueCreateInfo queue_info = {};
    float queue_priorities[1] = {0.0};
    queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queue_info.queueFamilyIndex = queue_family_index;
    queue_info.queueCount = 1;
    queue_info.pQueuePriorities = queue_priorities;

    VkDeviceCreateInfo device_info = {};
    device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
    device_info.queueCreateInfoCount = 1;
    device_info.pQueueCreateInfos = &queue_info;
    graphics_queue_family_index = queue_family_index;
    return vkCreateDevice(pdevice, &device_info, nullptr, &device);
  }

this example only accepts one queue, but you can do more with vulkan’s api. And you can pass the needed extensions at this time.

Summary

Summarize those steps, you need to

  • Start the Vulkan Instance
  • Find the right physical device, usually the first one
  • Find the right queue which is able to do graphics work
  • Create logical device from the selected queues of physical device
  • Create command buffer pool

Code is available at github

Refer to VulkanSamples