使用vulkan api首先要创建一个实例,即VkInstance对象。

简单来说调用vkCreateInstance这个函数即可创建实例,但是为了填充实例的描述对象VkInstanceCreateInfo,需要做大量的前置检测和配置工具。

VkInstance instance;
VkInstanceCreateInfo createInfo;
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);

填充应用信息

// vulkan的应用信息
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
 
// vulkan实例信息
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;

填充扩展

创建实例的时候要指定要使用的扩展信息。比如窗口扩展,不过因为vulkan是跨平台的,所以在不同平台的窗口系统之间加了一个中间层,我们需要获取这个中间层依赖的扩展以便和窗口系统进行交互。这一部分借助GLFW这个胶水库可以完成。

uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount)

在macos上,由于macos对于vulkan的支持还并不完善,因此需要额外启用一个扩展来允许 Vulkan 应用程序在不完全支持 Vulkan 标准的设备上运行

std::vector<const char*> requiredExtensions;
for (uint32_t i = 0; i < glfwExtensionCount; i++) {
  requiredExtensions.emplace_back(glfwExtensions[i]);
}
// macos上额外启动扩展
requiredExtensions.emplace_back(
	VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
// 启用这个扩展还需要设置对应的flag
createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;

这里我们只是将扩展的名称放入了vector当中,还需要填充到createInfo当中。即数组首地址和长度两部分

createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
createInfo.ppEnabledExtensionNames = requiredExtensions.data();

校验扩展

上面是我们设置要启用的扩展并进行填充。但是我们要使用的扩展并不一定被所在平台支持,因此可以进行校验。也就是读取该平台上支持的扩展,判断应用必须的扩展是否支持。

扩展的获取分为两部,一是获取扩展的长度,而是获取扩展的名称列表。两者调用的是同一个函数,第一次的调用只是为第二次接受数据的数据提前确认好预留的内存空间而已。

bool checkExtensionAvailable(
  const std::vector<const char*>& requiredExtension) {
// 获取可用的扩展
uint32_t availableExtensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &availableExtensionCount,
									   nullptr);
std::vector<VkExtensionProperties> availableExtensions(
	availableExtensionCount);
vkEnumerateInstanceExtensionProperties(nullptr, &availableExtensionCount,
									   availableExtensions.data());
// 检查必须扩展是否在可用扩展内
for (const char* extensionName : requiredExtension) {
  bool found = false;
  for (const auto& extensionProperties : availableExtensions) {
	if (strcmp(extensionName, extensionProperties.extensionName) == 0) {
	  found = true;
	  break;
	}
  }
  if (!found) {
	return false;
  }
}
return true;
}

填充校验层

vulkan的接口本身并没有什么错误处理,以及参数合法性的校验,需要开发者完全掌控vulkan的使用。

不过vulkan通过校验层可以帮助开发者在开发阶段检测和调试Vulkan API的使用错误。它们提供了额外的检查和报告机制,可以捕捉到许多常见的错误和潜在问题,从而提高代码的稳定性和可靠性。

在开发阶段启用校验层可以显著减少调试时间和难度,确保在发布阶段代码更加健壮。 例如下面,我们启用一个名为VK_LAYER_KHRONOS_validation的校验层,来捕获api的使用错误和潜在问题。使用方法和扩展类似,方法命名风格也是一致的。

const std::vector<const char*> validationLayers = {
	"VK_LAYER_KHRONOS_validation"};
createInfo.enabledLayerCount =
  static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();

当然,为了保证使用的校验层是有效的,可以在填充到createInfo之前进行校验,校验方法和扩展一样

bool createValidationLayers(
  const std::vector<const char*>& validationLayers) {
// 像获取可用扩展一样,获取可用验证层
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
 
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
// 所需的验证层不存在,则校验失败
for (const char* layerName : validationLayers) {
  bool layerFound = false;
  for (const auto& layerProperties : availableLayers) {
	if (strcmp(layerName, layerProperties.layerName) == 0) {
	  layerFound = true;
	  break;
	}
  }
 
  if (!layerFound) {
	return false;
  }
}
return true;
}

设置消息回调(可选)

上面我们通过设置校验层来捕获一些异常信息,默认是输出到标准输出的。这里我们还可以通过设置消息回调的方式来实现,而且更加灵活。

这里使用的是VK_EXT_debug_utils扩展以及设置它的回调。没错,我们还需要开启扩展之后,才能设置debug message的回调处理函数。

启用扩展

启用扩展很简单,在扩展数组添加即可

requiredExtensions.emplace_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);

填充createInfo

和创建vulkan实例需要填充实例的createInfo一样,创建debug message回调也要填充其对应的createInfo。

VkDebugUtilsMessengerCreateInfoEXT createInfo;
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
// 指定消息的级别
createInfo.messageSeverity =
	VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
	VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
	VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
// 指定消息的类型
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
						 VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
// 设置回调函数
createInfo.pfnUserCallback = debugCallback;

上面特别重要的就是回调函数的设置,我们的处理逻辑就在这里实现。按照指定的调用约定和函数签名来定义这个函数

static VKAPI_ATTR VkBool32 VKAPI_CALL
debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
			VkDebugUtilsMessageTypeFlagsEXT messageType,
			const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
			void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
  // Message is important enough to show
}
return VK_FALSE;
}

创建消息回调

其实就是调用一下对应的create函数了。

VkDebugUtilsMessengerEXT debugMessenger;
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr,
								 &debugMessenger) != VK_SUCCESS) {
  throw std::runtime_error("failed to set up debug messenger!");
}
 
VkResult CreateDebugUtilsMessengerEXT(
  VkInstance instance,
  const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo,
  const VkAllocationCallbacks* pAllocator,
  VkDebugUtilsMessengerEXT* pDebugMessenger) {
auto func = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
	instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr) {
  return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
} else {
  return VK_ERROR_EXTENSION_NOT_PRESENT;
}
}

在vulkan实例销毁前,也要销毁掉这个消息回调

void DestroyDebugUtilsMessengerEXT(VkInstance instance,
								 VkDebugUtilsMessengerEXT debugMessenger,
								 const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
	instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
  func(instance, debugMessenger, pAllocator);
}
}

vulkan实例的create和destroy消息回调

本小节设置的消息回调是在vulkan实例创建之后设置的,在vulkan实例销毁之前销毁的。所以在vulkan实例创建过程和销毁过程中,没有办法使用消息回调来处理调试信息。

为了解决这个问题,vulkan实例中有一个pNext字段,直接将其设置为上面创建消息回调填充的createInfo即可,这样就可以在vulkan实例create和destroy的时候也走这个逻辑了。

// 这里就是前面debugCreateInfo的封装函数
populateDebugMessengerCreateInfo(debugCreateInfo);
      createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*)&debugCreateInfo;

到此为止,我们学些到了vulkan实例的初始化流程。以及中间一些层,如扩展层、校验层、消息回调设置的方法。