注意,所有平台上该可执行文件都叫 `vkconfig`。
简介
本章将介绍 Vulkan 开发中重要的生态工具,并学习如何调整代码以支持更广泛的 GPU。随着 Vulkan 不断推出新版本和新特性,你需要理解如何:
-
了解不同 GPU 支持哪些特性
-
调整代码以保持对旧硬件的兼容性
-
在可用时有条件地使用高级特性
这些知识对于开发能在多种硬件上运行的 Vulkan 应用至关重要,从最新的高端 GPU 到较旧或性能有限的设备都涵盖在内。
Vulkan 硬件数据库(GPUInfo.org)
GPUInfo.org 简介
Vulkan 硬件数据库(GPUInfo.org)对 Vulkan 开发者来说是极有价值的资源。这个由社区维护的数据库收集并展示各类 GPU 与设备的 Vulkan 支持情况。
GPUInfo.org 提供以下详细信息:
-
支持的 Vulkan 版本
-
可用的扩展
-
特性支持情况
-
实现限制
-
格式属性
-
队列族属性
这些信息由用户众包而来:他们运行 Vulkan Hardware Capability Viewer 工具,把自己的 GPU 能力上报到数据库。
在开发中使用 GPUInfo.org
开发 Vulkan 应用时,GPUInfo.org 可以帮你:
-
确定最低要求:弄清为了支持你想要覆盖的硬件范围,需要以哪个 Vulkan 版本和哪些扩展作为目标。
-
检查特性可用性:确认动态渲染、时间线信号量、光线追踪等具体特性的支持是否普遍。
-
识别实现限制:了解各种 Vulkan 特性在不同硬件上的实际限制。
-
比较厂商与设备:了解 NVIDIA、AMD、Intel 以及移动 GPU 厂商在 Vulkan 支持上的差异。
下面看几个使用 GPUInfo.org 的实际例子:
示例:检查 Vulkan 版本支持
要弄清 Vulkan 1.3(引入动态渲染的版本)的支持面有多广:
-
访问 GPUInfo.org
-
进入“Core Version Support”页面
-
查看支持 Vulkan 1.3 的设备占比
你会发现,较新的 GPU 虽然支持 Vulkan 1.3 及以上,但仍有大量设备停留在 Vulkan 1.0、1.1 或 1.2。
示例:检查扩展支持
如果你在考虑使用某个扩展:
-
进入“Extensions”栏目
-
搜索你关注的扩展
-
查看它在不同厂商设备上的支持比例
这能帮你决定是强制要求该扩展,还是提供一条回退路径。
[[Example: Using the Vulkan Configurator Tool]] ==== 示例:使用 Vulkan Configurator 工具
Vulkan Configurator 工具(所有平台上可执行文件名都是 vkconfig)随 Vulkan SDK 一起提供,可以方便地配置系统中的 Vulkan 设置。用法如下:
-
启动 Vulkan Configurator:
-
Windows:建议从开始菜单启动“Vulkan Configurator”,因为在命令行运行
vkconfig.exe只会显示有限的选项 -
其他平台:打开终端并运行
vkconfig
-
-
配置验证层:
-
切换到“Layers”标签页
-
根据调试需要启用或停用特定验证层
-
例如在开发期间启用
VK_LAYER_KHRONOS_validation,以捕获 API 使用错误
-
-
管理环境变量:
-
进入“Settings”标签页
-
设置
VK_LAYER_PATH或VK_ICD_FILENAMES之类的环境变量 -
这些设置可以应用到整个系统,也可以只对当前会话生效
-
-
配置驱动专属选项:
-
部分 GPU 厂商提供了额外的配置选项
-
可以通过厂商专属的标签页访问这些选项
-
-
导出配置:
-
保存配置以便日后使用,或分享给团队成员
-
这样能保证多台开发机上的 Vulkan 环境保持一致
-
以下情况尤其适合使用 Vulkan Configurator: - 需要用不同验证层配置调试 Vulkan 应用时 - 不修改代码就想用不同 Vulkan 设置测试应用时 - 要搭建有特定 Vulkan 要求的开发环境时
用 Vulkan Configurator 代替代码管理验证层
很多 Vulkan 应用会在创建实例时以编程方式启用验证层,通常只在调试构建中这样做。常见写法如下:
// 定义验证层
const std::vector<char const*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
// 仅在调试构建中启用
#ifdef NDEBUG
constexpr bool enableValidationLayers = false;
#else
constexpr bool enableValidationLayers = true;
#endif
void createInstance() {
// 检查验证层是否可用
if (enableValidationLayers && !checkValidationLayerSupport()) {
throw std::runtime_error("validation layers requested, but not available!");
}
// 应用信息……
// 调试模式下启用验证层
std::vector<char const *> enabledLayers;
if (enableValidationLayers) {
enabledLayers.assign(validationLayers.begin(), validationLayers.end());
}
// 创建带验证层的实例
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo,
.enabledLayerCount = static_cast<uint32_t>(enabledLayers.size()),
.ppEnabledLayerNames = enabledLayers.data(),
// ……其他参数
};
instance = vk::raii::Instance(context, createInfo);
}
这种做法虽然可行,但有几个缺点:
-
启用或停用验证层都要修改代码并重新编译
-
想尝试不同的验证层配置更麻烦
-
增加了代码库的复杂度
更好的做法是用 Vulkan Configurator 在外部管理验证层。要利用这一点,可以这样改代码:
void createInstance() {
// 应用信息……
// 创建实例,但不显式启用验证层
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo,
// ……其他参数
};
instance = vk::raii::Instance(context, createInfo);
}
这样做的好处是:
-
应用里不再有任何验证层相关代码
-
需要时用 Vulkan Configurator 启用验证层
-
不用重新编译就能切换验证配置
用 Vulkan Configurator 启用验证层的步骤:
-
启动 Vulkan Configurator(Windows 上从开始菜单启动,或在终端运行
vkconfig——所有平台上该可执行文件都叫vkconfig) -
进入“Layers”标签页
-
启用
VK_LAYER_KHRONOS_validation验证层 -
应用设置
该配置会作用于该环境中的所有 Vulkan 应用,因此不修改代码就能轻松开关验证层。
这种做法的好处包括:
-
代码更干净:应用代码不必再处理验证层
-
更灵活:不重新编译就能更改验证设置
-
更一致:多个应用可以沿用同一套验证设置
-
便于试验:可以轻松尝试不同的验证配置
其他有用的生态工具
除 GPUInfo.org 之外,还有几款工具能在开发和调试 Vulkan 应用时帮上忙:
-
Vulkan SDK 工具:
-
vulkaninfo:显示本地系统的 Vulkan 能力 -
vkconfig(Vulkan Configurator):用于管理 Vulkan 设置的配置工具(详见 [Example: Using the Vulkan Configurator Tool]) -
验证层:帮助识别 API 使用错误
-
RenderDoc:图形调试工具
-
-
厂商专属工具:
-
NVIDIA Nsight Graphics
-
AMD Radeon GPU Profiler
-
支持较旧的 GPU
现在你已了解如何查明 GPU 能力,接下来看看怎样修改代码,以支持那些不具备动态渲染等 Vulkan 1.3/1.4 特性的旧 GPU。
检测可用特性
第一步是检测用户 GPU 上有哪些特性可用。这一步在创建设备时完成:
// 检查是否支持动态渲染
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;
动态渲染的替代方案:传统渲染通道
如果动态渲染不可用,就需要改用传统渲染通道和帧缓冲。这个替代方案可以这样实现:
创建渲染通道
void createRenderPass() {
if (appInfo.dynamicRenderingSupported) {
// 动态渲染下不需要渲染通道
return;
}
// 颜色附件描述
vk::AttachmentDescription colorAttachment{
.format = swapChainImageFormat,
.samples = vk::SampleCountFlagBits::e1,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::ePresentSrcKHR
};
// 子通道对颜色附件的引用
vk::AttachmentReference colorAttachmentRef{
.attachment = 0,
.layout = vk::ImageLayout::eColorAttachmentOptimal
};
// 子通道描述
vk::SubpassDescription subpass{
.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
.colorAttachmentCount = 1,
.pColorAttachments = &colorAttachmentRef
};
// 依赖关系,确保图像布局正确转换
vk::SubpassDependency dependency{
.srcSubpass = VK_SUBPASS_EXTERNAL,
.dstSubpass = 0,
.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
.srcAccessMask = vk::AccessFlagBits::eNone,
.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
};
// 创建渲染通道
vk::RenderPassCreateInfo renderPassInfo{
.attachmentCount = 1,
.pAttachments = &colorAttachment,
.subpassCount = 1,
.pSubpasses = &subpass,
.dependencyCount = 1,
.pDependencies = &dependency
};
renderPass = device.createRenderPass(renderPassInfo);
}
创建帧缓冲
void createFramebuffers() {
if (appInfo.dynamicRenderingSupported) {
// 动态渲染下不需要帧缓冲
return;
}
swapChainFramebuffers.resize(swapChainImageViews.size());
for (size_t i = 0; i < swapChainImageViews.size(); i++) {
vk::ImageView attachments[] = {
swapChainImageViews[i]
};
vk::FramebufferCreateInfo framebufferInfo{
.renderPass = renderPass,
.attachmentCount = 1,
.pAttachments = attachments,
.width = swapChainExtent.width,
.height = swapChainExtent.height,
.layers = 1
};
swapChainFramebuffers[i] = device.createFramebuffer(framebufferInfo);
}
}
修改管线创建
创建图形管线时,如果动态渲染不可用,就需要指定渲染通道:
void createGraphicsPipeline() {
// ……已有的着色器阶段和固定功能设置……
vk::GraphicsPipelineCreateInfo pipelineInfo{};
if (appInfo.dynamicRenderingSupported) {
// 使用动态渲染
vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{
.colorAttachmentCount = 1,
.pColorAttachmentFormats = &swapChainImageFormat
};
pipelineInfo.pNext = &pipelineRenderingCreateInfo;
pipelineInfo.renderPass = nullptr;
} else {
// 使用传统渲染通道
pipelineInfo.pNext = nullptr;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
}
// ……管线创建的其余部分……
}
调整命令缓冲区录制
最后,需要调整命令缓冲区的录制方式:
void recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t imageIndex) {
// ……开始录制命令缓冲区……
if (appInfo.dynamicRenderingSupported) {
// 开始动态渲染
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);
} else {
// 开始传统渲染通道
vk::RenderPassBeginInfo renderPassInfo{
.renderPass = renderPass,
.framebuffer = swapChainFramebuffers[imageIndex],
.renderArea = {{0, 0}, swapChainExtent},
.clearValueCount = 1,
.pClearValues = &clearColor
};
commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);
}
// ……绑定管线并绘制……
if (appInfo.dynamicRenderingSupported) {
commandBuffer.endRendering();
} else {
commandBuffer.endRenderPass();
}
// ……结束命令缓冲区……
}
处理其他 Vulkan 1.3/1.4 特性
动态渲染只是一个例子,旧 GPU 上不可用的特性还有很多。以下其他 Vulkan 1.3/1.4 特性可能也需要你准备替代方案:
时间线信号量
时间线信号量(Vulkan 1.2 引入)提供了比二元信号量更灵活的同步机制。如果它不可用,就需要改用二元信号量和栅栏:
bool timelineSemaphoresSupported = false;
// 检查是否支持 Vulkan 1.2 或该扩展
if (deviceProperties.apiVersion >= VK_API_VERSION_1_2) {
timelineSemaphoresSupported = true;
} else {
// 检查扩展
for (const auto& extension : availableExtensions) {
if (strcmp(extension.extensionName, VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME) == 0) {
timelineSemaphoresSupported = true;
break;
}
}
}
// 创建相应的同步原语
if (timelineSemaphoresSupported) {
// 创建时间线信号量
vk::SemaphoreTypeCreateInfo timelineCreateInfo{
.semaphoreType = vk::SemaphoreType::eTimeline,
.initialValue = 0
};
vk::SemaphoreCreateInfo semaphoreInfo{
.pNext = &timelineCreateInfo
};
timelineSemaphore = device.createSemaphore(semaphoreInfo);
} else {
// 创建二元信号量和栅栏
vk::SemaphoreCreateInfo semaphoreInfo{};
vk::FenceCreateInfo fenceInfo{.flags = vk::FenceCreateFlagBits::eSignaled};
for (size_t i = 0; i < swapChainImages.size(); i++)
{
renderFinishedSemaphores[i] = device.createSemaphore(semaphoreInfo);
}
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++)
{
imageAvailableSemaphores[i] = device.createSemaphore(semaphoreInfo);
inFlightFences[i] = device.createFence(fenceInfo);
}
}
Synchronization2
Synchronization2 特性(Vulkan 1.3)简化了管线屏障与内存依赖。如果它不可用,就使用原有的同步命令:
bool synchronization2Supported = false;
// 检查是否支持 Vulkan 1.3 或该扩展
if (deviceProperties.apiVersion >= VK_API_VERSION_1_3) {
synchronization2Supported = true;
} else {
// 检查扩展
for (const auto& extension : availableExtensions) {
if (strcmp(extension.extensionName, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME) == 0) {
synchronization2Supported = true;
break;
}
}
}
// 使用相应的屏障命令
if (synchronization2Supported) {
// 使用 Synchronization2 API
vk::ImageMemoryBarrier2 barrier{
.srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe,
.srcAccessMask = vk::AccessFlagBits2::eNone,
.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
.oldLayout = vk::ImageLayout::eUndefined,
.newLayout = vk::ImageLayout::eAttachmentOptimal,
.image = swapChainImages[i],
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}
};
vk::DependencyInfo dependencyInfo{
.imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &barrier
};
commandBuffer.pipelineBarrier2(dependencyInfo);
} else {
// 使用原有的同步 API
vk::ImageMemoryBarrier barrier{
.srcAccessMask = vk::AccessFlagBits::eNone,
.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
.oldLayout = vk::ImageLayout::eUndefined,
.newLayout = vk::ImageLayout::eColorAttachmentOptimal,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = swapChainImages[i],
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}
};
commandBuffer.pipelineBarrier(
vk::PipelineStageFlagBits::eTopOfPipe,
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::DependencyFlagBits::eByRegion,
{},
{},
{ barrier }
);
}
跨 GPU 兼容性的最佳实践
根据前面的内容,下面是一些最佳实践,帮助你开发能在各种 GPU 上运行的 Vulkan 应用:
-
在运行时检查特性可用性:不要只凭 Vulkan 版本就假定特性可用。始终检查具体的特性和扩展。
-
提供回退路径:为现代特性不可用的情况实现替代代码路径。
-
使用特性结构体:创建逻辑设备时,用相应的特性结构体只启用你确实需要且确实可用的特性。
-
在多种硬件上测试:借助 GPUInfo.org 找出常见的硬件配置,并在有代表性的样本上测试应用。
-
优雅降级:设计应用时,让它在性能较弱的硬件上平稳地降低画质或功能。
-
记录需求:清楚写明应用对 Vulkan 版本和扩展的最低要求与推荐要求。
小结
了解 Vulkan 生态工具、懂得针对不同 GPU 能力调整代码,是 Vulkan 开发者必备的技能。按照本章介绍的方法,你可以做出能在多种硬件上运行的应用,同时在条件允许时仍能利用最新特性。