目录
[创建实例(Creating an instance)](#创建实例(Creating an instance))
[遇到 VK_ERROR_INCOMPATIBLE_DRIVER 错误(Encountered VK_ERROR_INCOMPATIBLE_DRIVER)](#遇到 VK_ERROR_INCOMPATIBLE_DRIVER 错误(Encountered VK_ERROR_INCOMPATIBLE_DRIVER))
[检查扩展支持(Checking for extension support)](#检查扩展支持(Checking for extension support))
[清理资源(Cleaning up)](#清理资源(Cleaning up))
创建实例(Creating an instance)
你需要做的第一件事是通过创建实例(Instance)来初始化 Vulkan 库。实例是应用程序与 Vulkan 库之间的连接,创建实例时需要向驱动程序指定应用程序的一些相关细节。
首先添加一个 createInstance 函数,并在 initVulkan 函数中调用它:
cpp
void initVulkan() {
createInstance();
}
另外添加一个数据成员来存储实例的句柄:
cpp
private:
VkInstance instance;
现在,要创建实例,我们首先需要填充一个结构体,包含应用程序的相关信息。从技术上讲,这些数据是可选的,但它可能会向驱动程序提供一些有用的信息,以便优化我们的特定应用程序(例如,因为它使用了一个知名的图形引擎,具有某些特殊行为)。这个结构体名为 VkApplicationInfo:
cpp
void createInstance() {
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 中的许多结构体都要求你在 sType 成员中显式指定类型。这也是众多带有 pNext 成员的结构体之一,该成员在未来可以指向扩展信息。我们在这里使用值初始化,将其设为 nullptr。
Vulkan 中的很多信息是通过结构体而不是函数参数传递的,我们还需要再填充一个结构体,为创建实例提供足够的信息。下一个结构体是必填的,它告诉 Vulkan 驱动程序我们想要使用哪些全局扩展(global extensions)和验证层(validation layers)。这里的 "全局" 意味着它们适用于整个程序,而不是特定的设备,这一点在接下来的章节中会变得更加清晰。
cpp
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
前两个参数很直观。接下来的两个参数指定所需的全局扩展。如概述章节所述,Vulkan 是一个与平台无关的 API,这意味着你需要一个扩展来与窗口系统交互。GLFW 有一个便捷的内置函数,可以返回它所需的扩展,我们可以将其传递给该结构体:
cpp
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
createInfo.enabledExtensionCount = glfwExtensionCount;
createInfo.ppEnabledExtensionNames = glfwExtensions;
该结构体的最后两个成员决定了要启用的全局验证层。我们将在下一章中更深入地讨论这些内容,所以现在先将它们留空:
cpp
createInfo.enabledLayerCount = 0;
现在我们已经指定了 Vulkan 创建实例所需的所有信息,终于可以调用 vkCreateInstance 了:
cpp
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
你会发现,Vulkan 中对象创建函数参数的通用模式如下:
- 指向创建信息结构体的指针
- 指向自定义分配器回调的指针(本教程中始终为
nullptr) - 指向存储新对象句柄的变量的指针
如果一切顺利,实例的句柄将存储在 VkInstance 类成员中。几乎所有 Vulkan 函数都会返回一个 VkResult 类型的值,该值要么是 VK_SUCCESS,要么是一个错误代码。要检查实例是否创建成功,我们不需要存储结果,只需直接检查是否为成功值即可:
cpp
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
现在运行程序,确保实例创建成功。
遇到 VK_ERROR_INCOMPATIBLE_DRIVER 错误(Encountered VK_ERROR_INCOMPATIBLE_DRIVER)
如果在 macOS 上使用最新的 MoltenVK SDK,调用 vkCreateInstance 可能会返回 VK_ERROR_INCOMPATIBLE_DRIVER。根据入门说明(Getting Start Notes),从 1.3.216 版本的 Vulkan SDK 开始,VK_KHR_PORTABILITY_subset 扩展是必需的。
要解决此错误,首先需要向 VkInstanceCreateInfo 结构体的 flags 中添加 VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR 位,然后将 VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME 添加到实例的启用扩展列表中。
通常代码如下所示:
cpp
...
std::vector<const char*> requiredExtensions;
for(uint32_t i = 0; i < glfwExtensionCount; i++) {
requiredExtensions.emplace_back(glfwExtensions[i]);
}
requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size();
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
检查扩展支持(Checking for extension support)
如果你查看 vkCreateInstance 的文档,会发现其中一个可能的错误代码是 VK_ERROR_EXTENSION_NOT_PRESENT。我们可以直接指定所需的扩展,如果返回该错误代码就终止程序。这对于像窗口系统接口这样的核心扩展来说是合理的,但如果我们想要检查可选功能呢?
要在创建实例之前获取支持的扩展列表,可以使用 vkEnumerateInstanceExtensionProperties 函数。它接受一个指向存储扩展数量的变量的指针,以及一个用于存储扩展详细信息的 VkExtensionProperties 数组。它还有一个可选的第一个参数,允许我们按特定的验证层过滤扩展,目前我们先忽略这个参数。
要分配一个数组来存储扩展详细信息,我们首先需要知道扩展的数量。你可以通过留空后一个参数来仅请求扩展的数量:
cpp
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
现在分配一个数组来存储扩展详细信息(需要包含 <vector> 头文件):
cpp
std::vector<VkExtensionProperties> extensions(extensionCount);
最后,我们可以查询扩展的详细信息:
cpp
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
每个 VkExtensionProperties 结构体都包含一个扩展的名称和版本。我们可以通过一个简单的 for 循环来列出它们(\t 是用于缩进的制表符):
cpp
std::cout << "available extensions:\n";
for (const auto& extension : extensions) {
std::cout << '\t' << extension.extensionName << '\n';
}
如果你想提供有关 Vulkan 支持的一些详细信息,可以将此代码添加到 createInstance 函数中。作为一个练习,尝试创建一个函数,检查 glfwGetRequiredInstanceExtensions 返回的所有扩展是否都包含在支持的扩展列表中。
清理资源(Cleaning up)
VkInstance 应该只在程序退出前销毁。可以在 cleanup 函数中使用 vkDestroyInstance 函数来销毁它:
cpp
void cleanup() {
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
vkDestroyInstance 函数的参数很直观。如前一章所述,Vulkan 中的分配和释放函数都有一个可选的分配器回调,我们通过传递 nullptr 来忽略它。在后续章节中我们将创建的所有其他 Vulkan 资源,都应该在实例销毁之前清理干净。
在继续实例创建之后的更复杂步骤之前,是时候通过了解验证层(validation layers)来评估我们的调试选项了。