void initVulkan() {
createInstance();
}
你需要做的第一件事是通过创建一个*实例(Instance)*来初始化 Vulkan 库。实例是应用程序与 Vulkan 库之间的连接,创建它需要向驱动程序指定一些关于应用程序的细节。
首先添加一个 createInstance 函数,并在 initVulkan 函数中调用它。
void initVulkan() {
createInstance();
}
此外,添加一个数据成员来保存实例的句柄以及 RAII 上下文:
private:
vk::raii::Context context;
vk::raii::Instance instance = nullptr;
现在,要创建实例,我们首先需要填充一个包含应用程序信息的结构体。这些数据在技术上不是必需的,但它可以为驱动程序提供一些有用的信息,以便针对我们的特定应用程序进行优化(例如,因为它使用了具有某些特殊行为的知名图形引擎)。这个结构体叫做 vk::ApplicationInfo:
void createInstance()
{
constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle",
.applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.apiVersion = vk::ApiVersion14};
}
虽然 vk::ApiVersion10 或 Vulkan 1.0 确实存在,但某些功能较旧,无法很好地与 RAII 配合使用,或者正如我们将在 后面讨论 的那样,无法很好地与 Slang 语言配合,因此我们将 1.4 作为我们的基线版本。如果我们在 C API 中,则需要指定 sType 和 pNext,并且会非常冗长。你可能会在其他使用 C API 的 Vulkan 项目中看到这种情况。使用现代 C++ 模块可以为你处理这些。
Vulkan 中的大量信息是通过结构体而不是函数参数传递的,我们需要再填充一个结构体来为创建实例提供足够的信息。下一个结构体是必须的,它告诉 Vulkan 驱动程序我们想要使用哪些全局扩展和验证层。这里的“全局”意味着它们适用于整个程序,而不是特定的设备,这在接下来的几章中会变得更加清晰。
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo
};
这个结构体有一个名为 flags 的成员,我们将在本章后面处理它。 成员 pApplicationInfo 指向我们刚刚创建的 appInfo。 接下来是一个请求的层数组,最后是一个所需的全局扩展数组。正如概述章节中提到的,Vulkan 是一个平台无关的 API,这意味着你需要一个扩展来与窗口系统交互。GLFW 有一个方便的内置函数,可以返回它需要的扩展,我们可以将其传递给结构体:
// 从 GLFW 获取所需的实例扩展。
uint32_t glfwExtensionCount = 0;
auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
// 检查 Vulkan 实现是否支持所需的 GLFW 扩展。
auto extensionProperties = context.enumerateInstanceExtensionProperties();
for (uint32_t i = 0; i < glfwExtensionCount; ++i)
{
if (std::ranges::none_of(extensionProperties,
[glfwExtension = glfwExtensions[i]](auto const& extensionProperty)
{ return strcmp(extensionProperty.extensionName, glfwExtension) == 0; }))
{
throw std::runtime_error("Required GLFW extension not supported: " + std::string(glfwExtensions[i]));
}
}
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo,
.enabledExtensionCount = glfwExtensionCount,
.ppEnabledExtensionNames = glfwExtensions};
另一个缺失的部分是要启用的层。这里我们将讨论如何启用验证层,这是对任何项目来说最有用和最重要的层之一。我们将在下一章更深入地讨论这个问题,现在暂时将其留空。
现在我们已经指定了 Vulkan 创建实例所需的一切,最后可以创建 vk::raii::Instance 了:
instance = vk::raii::Instance(context, createInfo);
如你所见,Vulkan 中对象创建函数参数遵循的通用模式是:
指向包含创建信息的结构体的指针
指向自定义分配器回调的指针,在本教程中始终忽略,因为如果你使用默认设置,它是可选的。
指向设备、实例或上下文的指针,构造函数依赖于它
返回指向构造的 RAII 对象的指针。
如果一切顺利,则返回实例的句柄。我们可以通过使用 C++ 异常来检查是否一切正常。如果你不能使用 C++ 异常,你可以通过定义 VULKAN_HPP_NO_EXCEPTIONS 来关闭它们。然后,调用将返回一个包含 VKResult 和返回对象的 std::tuple。以下是在 Vulkan 调用中检查错误的示例:
try
{
vk::raii::Context context;
vk::raii::Instance instance(context, vk::InstanceCreateInfo{});
vk::raii::PhysicalDevice physicalDevice = instance.enumeratePhysicalDevices().front();
vk::raii::Device device(physicalDevice, vk::DeviceCreateInfo{});
// 使用 Vulkan 对象
vk::raii::Buffer buffer(device, vk::BufferCreateInfo{});
}
catch (const vk::SystemError& err)
{
std::cerr << "Vulkan error: " << err.what() << std::endl;
return 1;
}
catch (const std::exception& err)
{
std::cerr << "Error: " << err.what() << std::endl;
return 1;
}
或者使用 tuple:
vk::raii::Context context;
...
auto instanceRV = context.createInstance(...);
if (!instanceRV.has_value())
{
std::cerr << "Error: Instance creation failed with " << vk::to_string(instanceVR.result) << std::endl;
std::exit(EXIT_FAILURE);
}
vk::raii::Instance instance = std::move(instanceRV.value);
...
auto physicalDevicesRV = instance.enumeratePhysicalDevices();
if (!physicalDevicesRV.hasValue())
{
std::cerr << "Error: Enumerating PhysicalDevices failed with " << vk::to_string(physicalDevicesRV) << std::endl;
std::exit(EXIT_FAILURE);
}
vk::raii::PhysicalDevice physicalDevice = std::move(physicalDevicesRV.value.front());
...
这个例子来自我们教程的后面部分,它只是展示了如何在所有调用中检查错误的一个示例。
如果在 macOS 上使用最新的 MoltenVK SDK,你可能会从 vk::raii::createInstance 或 vk::raii::Instance 构造函数中收到 vk::Result::eErrorIncompatibleDriver。根据 入门说明。
从 Vulkan SDK 1.3.216 开始,VK_KHR_PORTABILITY_subset 扩展是必需的。
要解决此错误,首先将 vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR 位添加到 vk::InstanceCreateInfo 结构体标志中,然后将 vk::KHRPortabilityEnumerationExtensionName 添加到实例启用的扩展列表中。
通常,代码如下所示:
constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle",
.applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.apiVersion = vk::ApiVersion14};
vk::InstanceCreateInfo createInfo{
.flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
.pApplicationInfo = &appInfo,
.ppEnabledExtensionNames = { vk::KHRPortabilityEnumerationExtensionName }
};
instance = vk::raii::Instance(m_context, createInfo);
如果你查看 vkCreateInstance 的文档,你会发现可能的错误代码之一是 VK_ERROR_EXTENSION_NOT_PRESENT。我们可以简单地指定我们需要的扩展,并在返回该错误代码时终止程序。这对于像窗口系统接口这样的必要扩展是有意义的,但是如果我们要检查可选功能呢?
要在创建实例之前检索支持的扩展列表,可以使用 vk::raii::Context::enumerateInstanceExtensionProperties 函数。它返回可用扩展的向量,这允许我们按特定的验证层过滤扩展,我们现在暂时忽略这一点。
auto extensions = context.enumerateInstanceExtensionProperties();
每个 vk::ExtensionProperties 结构体都包含扩展的名称和版本。我们可以使用一个简单的 for 循环列出它们(\t 是缩进的制表符):
std::cout << "available extensions:\n";
for (const auto& extension : extensions) {
std::cout << '\t' << extension.extensionName << '\n';
}
如果你希望提供有关 Vulkan 支持的一些详细信息,可以将此代码添加到 createInstance 函数中。
在继续实例创建之后更复杂的步骤之前,是时候通过查看 验证层 来评估我们的调试选项了。
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。