创建实例

您需要做的第一件事是通过创建一个实例来初始化 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 资源都应在实例销毁之前清理干净。

在继续实例创建之后更复杂的步骤之前,是时候通过查看验证层来评估我们的调试选项了。

C++ 代码