Vulkan 教程 Khronos Vulkan® Tutorial · 中文翻译 GitHub

引言

本章来探讨 Vulkan 配置文件(profile),这一强大特性建立在前一章介绍的生态工具之上。Vulkan 配置文件提供了一套标准化方式,可以:

  1. 定义应用所需的一组特性、扩展和限制

  2. 自动检查与用户硬件的兼容性

  3. 免去手动进行特性检测和编写回退路径的需要

  4. 大幅减少样板代码

如果希望应用能在各种硬件上稳定运行,又不想应付手动检查特性支持的繁琐,Vulkan 配置文件就特别有价值。

认识 Vulkan 配置文件

什么是 Vulkan 配置文件?

Vulkan 配置文件是功能、扩展、限制和格式的预定义集合,代表某个特定的目标环境或一整套最佳实践。它们在底层 Vulkan API 之上提供了更高层的抽象,使以下工作更容易:

  • 面向特定的硬件能力

  • 确保在不同 GPU 上的兼容性

  • 一致地落实最佳实践

  • 减少特性检测的样板代码

不必再逐项手动检查特性和扩展、编写回退路径,只需指定应用所需的配置文件即可。Vulkan Profiles 库会负责兼容性检查,并在用户硬件不满足要求时给出恰当的报错信息。

Vulkan 配置文件的类型

可用的配置文件有几种类型:

  1. API 配置文件:代表特定的 Vulkan API 版本(例如 Vulkan 1.1、1.2、1.3)

  2. 厂商配置文件:面向特定硬件厂商(例如 NVIDIA、AMD、Intel)

  3. 平台配置文件:面向特定平台(例如 Windows、Linux、Android)

  4. 最佳实践配置文件:落实 Vulkan 开发中推荐的做法

本章以最佳实践配置文件为例, 同时演示配置文件如何通过免去手动特性检测来简化你的代码。

配置文件如何简化你的代码

免去手动特性检测

到目前为止,我们都得手动检查特性支持并实现 回退路径:

  1. 检查设备是否支持 Vulkan 1.3

  2. 如果不支持,检查是否支持动态渲染扩展

  3. 两者都不支持时,回退到传统的渲染通道

  4. 对每个特性(时间线信号量、synchronization2 等)重复这一过程

  5. 为每个特性维护独立的代码路径

这种做法会产生大量条件分支,代码复杂且难以维护。

使用配置文件后,整个过程简化为:

  1. 检查配置文件是否受支持

  2. 如果支持,就使用该配置文件保证的全部特性

  3. 如果不支持,可以选择回退到更基础的做法

使用配置文件的好处

使用配置文件有若干好处:

  1. 大幅降低代码复杂度:不必再写大量的特性检查和条件分支

  2. 更易维护:需要测试和调试的代码路径更少

  3. 面向未来:新的 Vulkan 版本发布后,只需更新配置文件,无需改动你的代码

  4. 需求更清晰:配置文件明确说明了应用需要什么

  5. 简化错误处理:一次检查取代多次检查

在应用中实现配置文件

下面看如何在 Vulkan 应用中实现配置文件。我们会以最佳实践配置文件为例,说明配置文件如何取代前一章不得不做的手动特性检测。

添加 Vulkan Profiles 库

首先需要包含 Vulkan Profiles 的头文件:

#include <vulkan/vulkan_profiles.hpp>

这个头文件提供了使用 Vulkan 配置文件所需的函数和结构体。

Vulkan Profiles 头文件并不属于标准 Vulkan 头文件的一部分。 只有使用 Vulkan SDK 时才拿得到它。请确认已安装 Vulkan SDK,并在开发环境中正确配置。

定义配置文件的需求

不必再手动检查特性和扩展,直接声明配置文件的需求即可:

// 定义要使用的配置文件
const VpProfileProperties profile = {
    VP_KHR_ROADMAP_2022_NAME,
    VP_KHR_ROADMAP_2022_SPEC_VERSION
};

// 检查该配置文件是否受支持
VkBool32 supported = false;
vpGetPhysicalDeviceProfileSupport(instance, physicalDevice, &profile, &supported);

if (!supported) {
    throw std::runtime_error("Roadmap 2022 profile is not supported on this device");
}

使用配置文件创建设备

创建逻辑设备时,可以用配置文件自动启用所需的特性和扩展:

// 使用最佳实践配置文件创建设备
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

// 设置队列创建信息
// ...

// 在创建设备时应用最佳实践配置文件
vpCreateDevice(physicalDevice, &deviceCreateInfo, &bestPracticesProfile, nullptr, &device);

这会自动启用最佳实践配置文件要求的全部特性和扩展,无需手动逐个指定。

使用配置文件特有的特性

最佳实践配置文件会启用一些特定特性,你可以在应用中使用它们:

// 该配置文件保证这些特性可用
// 无需检查支持情况,也无需提供回退路径

// 示例:使用动态渲染(由该配置文件保证)
vk::RenderingAttachmentInfo colorAttachment{
    .imageView = swapChainImageViews[imageIndex],
    .imageLayout = vk::ImageLayout::eAttachmentOptimal,
    .loadOp = vk::AttachmentLoadOp::eClear,
    .storeOp = vk::AttachmentStoreOp::eStore,
    .clearValue = clearColor
};

vk::RenderingInfo renderingInfo{
    .renderArea = {{0, 0}, swapChainExtent},
    .layerCount = 1,
    .colorAttachmentCount = 1,
    .pColorAttachments = &colorAttachment
};

commandBuffer.beginRendering(renderingInfo);
// ... 绘制命令 ...
commandBuffer.endRendering();

配置文件的错误处理

使用配置文件时,错误处理会变得更直接:

try {
    // 尝试用最佳实践配置文件创建设备
    vpCreateDevice(physicalDevice, &deviceCreateInfo, &bestPracticesProfile, nullptr, &device);
} catch (const std::exception& e) {
    // 配置文件不受支持,给出对用户友好的错误信息
    std::cerr << "Your GPU does not support the required Vulkan features for optimal performance." << std::endl;
    std::cerr << "Error: " << e.what() << std::endl;

    // 可选:改用更基础的配置文件,或者优雅退出
    // ...
}

手动特性检测与配置文件的对比

对比这两种做法,就能看出配置文件能省掉多少代码和复杂度:

手动特性检测(前一章)

在前一章,每要使用一个特性,都得写这样的代码:

// 检查是否支持动态渲染
bool dynamicRenderingSupported = false;

// 检查是否支持 Vulkan 1.3
if (deviceProperties.apiVersion >= VK_API_VERSION_1_3) {
    dynamicRenderingSupported = true;
} else {
    // 在较旧的 Vulkan 版本上检查该扩展
    for (const auto& extension : availableExtensions) {
        if (strcmp(extension.extensionName, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) == 0) {
            dynamicRenderingSupported = true;
            break;
        }
    }
}

// 保存这一信息以便后续使用
appInfo.dynamicRenderingSupported = dynamicRenderingSupported;

然后还得在整个应用中编写条件分支代码:

// 创建管线时
if (appInfo.dynamicRenderingSupported) {
    // 使用动态渲染
    vk::PipelineRenderingCreateInfo renderingInfo{
        .colorAttachmentCount = 1,
        .pColorAttachmentFormats = &swapChainImageFormat
    };
    pipelineInfo.pNext = &renderingInfo;
    pipelineInfo.renderPass = nullptr;
} else {
    // 使用传统渲染通道
    pipelineInfo.pNext = nullptr;
    pipelineInfo.renderPass = renderPass;
    pipelineInfo.subpass = 0;
}

// 记录命令缓冲区时
if (appInfo.dynamicRenderingSupported) {
    // 开始动态渲染
    vk::RenderingAttachmentInfo colorAttachment{/*...*/};
    vk::RenderingInfo renderingInfo{/*...*/};
    commandBuffer.beginRendering(renderingInfo);
} else {
    // 开始传统渲染通道
    vk::RenderPassBeginInfo renderPassInfo{/*...*/};
    commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);
}

// 命令缓冲区末尾还要再来一次
if (appInfo.dynamicRenderingSupported) {
    commandBuffer.endRendering();
} else {
    commandBuffer.endRenderPass();
}

每要条件性地使用一个特性(时间线信号量、synchronization2 等),都得重复这一套写法,最终得到的是复杂、分支繁多且难以维护的代码。

使用配置文件(本章)

使用配置文件后,所有这些复杂度都归结为:

// 定义配置文件
const VpProfileProperties profile = {
    VP_KHR_ROADMAP_2022_NAME,
    VP_KHR_ROADMAP_2022_SPEC_VERSION
};

// 检查该配置文件是否受支持
VkBool32 supported = false;
vpGetPhysicalDeviceProfileSupport(instance, physicalDevice, &profile, &supported);

if (supported) {
    // 用该配置文件创建设备——所有特性自动启用
    vpCreateDevice(physicalDevice, &deviceCreateInfo, &profile, nullptr, &device);

    // 现在可以使用该配置文件保证的任何特性,无需检查
    // 例如动态渲染总是可用的:
    vk::RenderingAttachmentInfo colorAttachment{/*...*/};
    vk::RenderingInfo renderingInfo{/*...*/};
    commandBuffer.beginRendering(renderingInfo);
    // ... 绘制命令 ...
    commandBuffer.endRendering();
}

采用配置文件的做法免去了:

  1. 多次特性检测

  2. 遍布应用的条件分支

  3. 在应用状态中跟踪特性支持情况的需要

  4. 维护和测试多条代码路径的复杂度

这样得到的代码:

  1. 显著更短

  2. 更易阅读和理解

  3. 更不容易出错

  4. 更易维护和更新

使用配置文件的最佳实践

使用 Vulkan 配置文件时,请考虑以下最佳实践:

  1. 选择合适的配置文件:所选配置文件应匹配应用的需求,同时不要过于严格。

  2. 提供回退方案:如果最佳实践配置文件不被支持,可以考虑回退到更基础的配置文件。

  3. 清楚说明需求:根据你所支持的配置文件,告知用户相应的硬件要求。

  4. 在多种硬件上测试:即便有了配置文件,也应在不同 GPU 上测试应用。

  5. 保持更新:配置文件会随新的 Vulkan 版本演进,因此要让实现保持最新。

结语

Vulkan 配置文件通过免去手动特性检测和条件分支代码,提供了简化 Vulkan 代码的有力方式。正如本章所见,配置文件能显著减少需要编写和维护的代码量,让你的应用:

  1. 更简洁易读

  2. 更易维护和更新

  3. 更不容易出错

  4. 在不同硬件上表现更一致

本章的示例展示了配置文件如何取代前一章不得不实现的复杂特性检测和回退路径。借助配置文件,你可以把更多精力放在应用的核心功能上,少花些心思应对硬件兼容性的琐碎细节。