Vulkan大闷锅之启程

事出有因

现在回想起来,那已经是几周前的事了。这是个干燥的上午,我正迷失在信息流中,角落的一行文字引起了注意,“OpenGL已经过时了”。过时了?不等我收拾心神,赶紧Google。霎时间,屏幕上迸出几个大字——V-u-l-k-a-n

Vulkan?干什么用的?能吃么?

就在那时,空气中弥漫着对无知的恐惧。吓得我赶紧学习了一下。

简述

Vulkan算是后继OpenGL的图形接口(不过Vulkan正式发布后,OpenGL居然还更新了…)。为保证准确性,本系列文不打算延伸,其核心的目标是,让程序跑起来。由于Vulkan诞生时间尚短,而且热度不高,仅凭网上几篇文档,让程序正常运行真的是有点难度。

本系列文是一个教程,而是一个参考。如果你和之前的我一样,花了大量时间,结果程序都跑不起来先。那么,欢迎阅读以下内容。

创建实例

由于Vulkan提供更直接的GPU控制,我们不能像OpenGL似的编程。逻辑设备(Logical Device)在这里起到了关键作用。当然,以Vulkan的尿性不可能直接让你创建逻辑设备。你需要先创建一个实例(Instance)。当然,创建实例也不简单,先看代码:

VkApplicationInfo app_info = {};
app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app_info.pNext = nullptr;
app_info.pApplicationName = APP_SHORT_NAME;
app_info.applicationVersion = 1;
app_info.pEngineName = APP_SHORT_NAME;
app_info.engineVersion = 1;
app_info.apiVersion = VK_API_VERSION_1_0;

VkInstanceCreateInfo inst_info = {};
inst_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
inst_info.pNext = nullptr;
inst_info.flags = 0;
inst_info.pApplicationInfo = &app_info;
//decide and init extensions
std::vector<const char *> extensions = { 
  VK_KHR_SURFACE_EXTENSION_NAME,
#if defined(VK_USE_PLATFORM_WIN32_KHR)
  VK_KHR_WIN32_SURFACE_EXTENSION_NAME
#elif defined(VK_USE_PLATFORM_XCB_KHR)
  VK_KHR_XCB_SURFACE_EXTENSION_NAME
#elif defined(VK_USE_PLATFORM_XLIB_KHR)
  VK_KHR_XLIB_SURFACE_EXTENSION_NAME
#endif
};
initExtensions(extensions);
inst_info.enabledExtensionCount = extensions.size();
inst_info.ppEnabledExtensionNames = extensions.data();

inst_info.enabledLayerCount = 0;
inst_info.ppEnabledLayerNames = nullptr;

VkInstance inst;
VkResult res = vkCreateInstance(&inst_info, nullptr, &inst);
if (res == VK_ERROR_INCOMPATIBLE_DRIVER)
{
  std::cout << "cannot find a compatible Vulkan ICD\n";
  exit(-1);
}
else if (res)
{
  std::cout << "unknown error\n";
  exit(-1);
}

首先,每个Vulkan程序都要先创建实例,而每个实例需要配置应用(Application)信息。几乎每个Vulkan对象的创建,都需要先配置创建信息(Create Info)。之所以有这么多配置,当然是因为控制权(可配置参数越多,控制权越大)。是不是感觉作者是控制狂?没关系,这才是个开头。

这里可以看出来,Vulkan的接口风格和OpenGL同样,都是入参出参,返回错误码,没见过的话会感觉非常别扭。不过这种周旋于CPU和GPU之间的(分布式)程序,貌似没有更好的接口设计了。这里面有两行配置会大量重复出现:

  • sType – 当前结构类型,用于校验或表示组建扩展。
  • pNext – 指向组建扩展的指针,大多数情况为空。

暂时看,配置信息大多是描述性的。这里要注意的是,我们直接启用了实例扩展(Instance Extension)。[showhide more_text=”这些函数用于验证实例是否支持扩展,单击展开” less_text=”收缩”]

bool CheckExtensionAvailability(
    const char *extension_name,
    std::vector<VkExtensionProperties> available_extensions)
{
  for (size_t i = 0; i < available_extensions.size(); ++i)
  {
    if (strcmp(available_extensions[i].extensionName, extension_name) == 0)
    {
      std::cerr << "extension name is: " << extension_name << std::endl;
      return true;
    }
  }
  return false;
}

bool initExtensions(std::vector<const char *> &extensions)
{
  uint32_t extensions_count = 0;
  if ((vkEnumerateInstanceExtensionProperties(nullptr, &extensions_count,
                                              nullptr) != VK_SUCCESS) ||
      (extensions_count == 0))
  {
    std::cerr << "Error" << std::endl;
    return false;
  }
  else
  {
    std::cout << extensions_count << " extensions" << std::endl;
  }

  std::vector<VkExtensionProperties> available_extensions(extensions_count);
  if (vkEnumerateInstanceExtensionProperties(nullptr, &extensions_count,
                                             available_extensions.data()) !=
      VK_SUCCESS)
  {
    std::cerr << "Error " << std::endl;
    return false;
  }

  for (uint32_t i = 0; i < extensions.size(); ++i)
  {
    if (!CheckExtensionAvailability(extensions[i], available_extensions))
    {
      std::cout << "could not find instance extension named \"" << extensions[i]
                << "\"!" << std::endl;
      return false;
    }
  }
  return true;
}

[/showhide]

除了glm没有依赖任何第三方库,正常地使用GUI接口需要提前定义宏,像这样(我使用Fedora系统,所以启用了XCB接口):

#define VK_USE_PLATFORM_XCB_KHR

如果你使用GLFW或者SDL创建窗口则不虚定义。

获取物理设备

要创建逻辑设备,我们得先拿到物理设备(Physical device),也就是GPU。

uint32_t gpu_count;
res = vkEnumeratePhysicalDevices(inst, &gpu_count, nullptr);
assert(res == VK_SUCCESS);
VkPhysicalDevice gpus[gpu_count];
res = vkEnumeratePhysicalDevices(inst, &gpu_count, gpus);
assert(res == VK_SUCCESS && gpu_count >= 1);

这里的设计比较独特,可以看这个函数签名:

VkResult 
vkEnumeratePhysicalDevices(VkInstance instance, 
                       uint32_t *pPhysicalDeviceCount, 
                       VkPhysicalDevice *pPhysicalDevices)

pPhysicalDevices为空,pPhysicalDeviceCount作为出参被写入当前GPU数量。

pPhysicalDevices不为空,pPhysicalDeviceCount作为入参,表示pPhysicalDevices的尺寸。

这样的设计在Vulkan中应用普遍。

看起来,距离逻辑设备就差一步,理论上只要这样:

vkCreateDevice(gpus[0], &device_info, nullptr, &device);

最多配置个创建信息。当然,以Vulkan的尿性,这里要先配置和创建指令队列(Queue)。

准备队列信息

OpenGL将指令统一发送给全局上下文处理,但Vulkan没有全局上下文。所以指令通常暂存在指令缓冲区(Command Buffer),通过提交给指令队列供GPU异步执行。Vulkan根据不同的队列类型将其排列成队列族(Queue Family)。队列族的定义是这样的:

typedef struct VkQueueFamilyProperties {
    VkQueueFlags    queueFlags;
    uint32_t        queueCount;
    uint32_t        timestampValidBits;
    VkExtent3D      minImageTransferGranularity;
} VkQueueFamilyProperties;

typedef enum VkQueueFlagBits {
    VK_QUEUE_GRAPHICS_BIT = 0x00000001,
    VK_QUEUE_COMPUTE_BIT = 0x00000002,
    VK_QUEUE_TRANSFER_BIT = 0x00000004,
    VK_QUEUE_SPARSE_BINDING_BIT = 0x00000008,
} VkQueueFlagBits;

和之前的接口类似,下面通过两次调用vkGetPhysicalDeviceQueueFamilyProperties拿到所有队列族的属性。

uint32_t queue_family_count;
vkGetPhysicalDeviceQueueFamilyProperties(gpus[0], &queue_family_count,
                                           nullptr);

std::cout << "found queue family: " << queue_family_count << std::endl; 
VkQueueFamilyProperties queue_props[queue_family_count]; 
vkGetPhysicalDeviceQueueFamilyProperties(gpus[0], 
                                 &queue_family_count, queue_props); 
assert(queue_family_count >= 1);

可以尝试打印一下:

for (auto &prop : queue_props)
{
  std::cout << prop.queueFlags << std::endl;
}

我这里输出15和4。也就是全Flag支持和TRANSFER。我们需要的是一条可以处理图形指令的队列,也就是第一条。这里的设计比较独特,表示队列族类型的是一个索引而不是handle。

VkDeviceQueueCreateInfo queue_info = {};
for (unsigned int i = 0; i < queue_family_count; ++i)
{
  if (queue_props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
  {
    queue_info.queueFamilyIndex = i;
    break;
  }
}

拿到索引值,写入配置。这里queue_properties表示多个Queue的优先级,我们只有一条队列,赋值为{0.0}即可。

float queue_priorities[1] = {0.0};
queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info.pNext = nullptr;
queue_info.queueCount = 1;
queue_info.pQueuePriorities = queue_priorities;
queue_info.queueFamilyIndex = queue_info.queueFamilyIndex;

在创建逻辑设别之前,需要手动初始化Surface。这是一项繁琐的工作。由于篇幅的限制,其余内容将留到后续的博文。

Leave a Reply

Your email address will not be published. Required fields are marked *