#include <vulkan/vulkan_profiles.hpp>
在本章中,我们将探讨 Vulkan Profiles,这是建立在我们上一章讨论的生态工具基础上的一个强大特性。Vulkan Profiles 提供了一种标准化的方式来:
定义你的应用程序所需的一组特性、扩展和限制
自动检查与用户硬件的兼容性
消除手动特性检测和回退路径的需求
显著减少样板代码
对于希望确保其应用程序在广泛硬件上一致运行,而又不想复杂地手动检查特性支持的开发者来说,Vulkan Profiles 尤其有价值。
Vulkan Profiles 是特性、扩展、限制和格式的预定义集合,代表特定的目标环境或一组最佳实践。它们在底层 Vulkan API 之上提供了一个更高层次的抽象,使得以下操作更加容易:
针对特定的硬件能力
确保不同 GPU 之间的兼容性
一致地实现最佳实践
减少特性检测的样板代码
你无需手动检查每个特性和扩展并实现回退路径,只需简单指定你的应用程序所需的 Profile。Vulkan Profiles 库将处理兼容性检查,并在用户硬件不满足要求时提供适当的错误信息。
有几种类型的 Profile 可用:
API Profiles:代表特定的 Vulkan API 版本(例如,Vulkan 1.1、1.2、1.3)
Vendor Profiles:针对特定的硬件厂商(例如,NVIDIA、AMD、Intel)
Platform Profiles:针对特定的平台(例如,Windows、Linux、Android)
Best Practices Profile:实现 Vulkan 开发的推荐实践
在本章中,我们将以 Best Practices Profile 为例,另外,我们将演示 Profiles 如何通过消除手动特性检测的需求来简化你的代码。
到目前为止,我们不得不手动检查特性支持并实现回退路径:
检查设备是否支持 Vulkan 1.3
如果不支持,检查它是否支持动态渲染扩展
如果两者都不支持,回退到传统的渲染通道
对每个特性(时间线信号量、synchronization2 等)重复此过程
为每个特性维护单独的代码路径
这种方法导致代码复杂、难以维护,并且有多个条件分支。
使用 Profiles,整个过程简化为:
检查 Profile 是否受支持
如果受支持,使用 Profile 保证的所有特性
如果不支持,可选择回退到更基础的方法
使用 Profiles 有几个优点:
大幅降低代码复杂度:无需多个特性检查和条件分支
提高可维护性:需要测试和调试的代码路径更少
面向未来:随着新 Vulkan 版本的发布,无需更改代码即可更新 Profiles
更清晰的需求:Profiles 提供了你的应用程序需要的明确规范
简化的错误处理:一次检查代替多次检查
让我们看看如何在你的 Vulkan 应用程序中实现 Profiles。我们将以 Best Practices Profile 为例,演示 Profiles 如何替代我们在上一章中不得不做的手动特性检测。
首先,你需要包含 Vulkan Profiles 头文件:
#include <vulkan/vulkan_profiles.hpp>
此头文件提供了使用 Vulkan Profiles 所需的函数和结构体。
Vulkan Profiles 头文件不是标准 Vulkan 头文件的一部分。只有使用 Vulkan SDK 时才可用。请确保你已在开发环境中安装并正确配置了 Vulkan SDK。
无需手动检查特性和扩展,你可以定义你的 Profile 需求:
// 定义我们要使用的 Profile
const VpProfileProperties profile = {
VP_KHR_ROADMAP_2022_NAME,
VP_KHR_ROADMAP_2022_SPEC_VERSION
};
// 检查 Profile 是否受支持
VkBool32 supported = false;
vpGetPhysicalDeviceProfileSupport(instance, physicalDevice, &profile, &supported);
if (!supported) {
throw std::runtime_error("Roadmap 2022 profile is not supported on this device");
}
当创建逻辑设备时,你可以使用 Profile 自动启用所需的特性和扩展:
// 使用 Best Practices Profile 创建设备
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
// 设置队列创建信息
// ...
// 将 Best Practices Profile 应用于设备创建
vpCreateDevice(physicalDevice, &deviceCreateInfo, &bestPracticesProfile, nullptr, &device);
这会自动启用 Best Practices Profile 所需的所有特性和扩展,无需手动指定。
Best Practices Profile 可能会启用你可以在应用程序中使用的特定特性:
// Profile 保证这些特性可用
// 无需检查支持或提供回退路径
// 示例:使用动态渲染(由 Profile 保证)
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();
使用 Profiles 时,错误处理变得更加直接:
try {
// 尝试使用 Best Practices Profile 创建设备
vpCreateDevice(physicalDevice, &deviceCreateInfo, &bestPracticesProfile, nullptr, &device);
} catch (const std::exception& e) {
// Profile 不受支持,提供用户友好的错误信息
std::cerr << "Your GPU does not support the required Vulkan features for optimal performance." << std::endl;
std::cerr << "Error: " << e.what() << std::endl;
// 可选择尝试使用更基础的 Profile 或优雅退出
// ...
}
让我们比较这两种方法,以了解 Profiles 可以消除多少代码和复杂度:
在上一章中,我们不得不为 每个 我们想使用的特性编写类似这样的代码:
// 检查是否支持动态渲染
bool dynamicRenderingSupported = false;
// 检查 Vulkan 1.3 支持
if (deviceProperties.apiVersion >= VK_API_VERSION_1_3) {
dynamicRenderingSupported = true;
} else {
// 在旧版本上检查扩展
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 等)重复这种模式,导致复杂、分支的代码,难以维护。
使用 Profiles,所有这些复杂度都减少为:
// 定义 Profile
const VpProfileProperties profile = {
VP_KHR_ROADMAP_2022_NAME,
VP_KHR_ROADMAP_2022_SPEC_VERSION
};
// 检查 Profile 是否受支持
VkBool32 supported = false;
vpGetPhysicalDeviceProfileSupport(instance, physicalDevice, &profile, &supported);
if (supported) {
// 使用 Profile 创建设备 - 自动启用所有特性
vpCreateDevice(physicalDevice, &deviceCreateInfo, &profile, nullptr, &device);
// 现在我们可以使用 Profile 保证的任何特性而无需检查
// 例如,动态渲染始终可用:
vk::RenderingAttachmentInfo colorAttachment{/*...*/};
vk::RenderingInfo renderingInfo{/*...*/};
commandBuffer.beginRendering(renderingInfo);
// ... 绘制命令 ...
commandBuffer.endRendering();
}
Profile 方法消除了:
多个特性检测检查
整个应用程序中的条件代码路径
在应用程序状态中跟踪特性支持的需求
维护和测试多个代码路径的复杂性
这使得代码:
显著更短
更易于阅读和理解
更不容易出错
更易于维护和更新
在使用 Vulkan Profiles 时,考虑以下最佳实践:
选择正确的 Profile:选择一个与你的应用程序需求匹配但不过度限制的 Profile。
提供回退选项:如果 Best Practices Profile 不受支持,考虑回退到更基础的 Profile。
清晰地传达需求:根据你支持的 Profiles 通知用户硬件要求。
在各种硬件上测试:即使使用 Profiles,在不同的 GPU 上测试你的应用程序仍然很重要。
保持更新:Profiles 随新的 Vulkan 版本而演变,因此保持你的实现是最新的。
Vulkan Profiles 提供了一种强大的方式来简化你的 Vulkan 代码,消除手动特性检测和条件代码路径的需求。正如我们在本章中所看到的,Profiles 可以大幅减少你需要编写和维护的代码量,使你的应用程序:
更简洁、更可读
更易于维护和更新
更不容易出错
在不同硬件上更一致
我们在本章中探讨的示例演示了 Profiles 如何替代我们在上一章中不得不实现的复杂特性检测和回退路径。通过使用 Profiles,你可以更多地关注应用程序的核心功能,而少操心硬件兼容性的复杂性。
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。