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