狸村 Mystic Web · 狸村
目录

引言

在本章中,我们将探讨 Vulkan 开发中重要的生态工具,并学习如何调整我们的代码以支持更广泛的 GPU。随着 Vulkan 不断推出新版本和新功能,理解以下内容非常重要:

  1. 如何发现不同 GPU 支持的功能

  2. 如何修改代码以保持与旧硬件的兼容性

  3. 如何根据条件使用高级功能

这些知识对于开发能够在各种硬件上运行的 Vulkan 应用程序至关重要,从最新的高端 GPU 到较旧或功能有限的设备。

Vulkan 硬件数据库(GPUInfo.org)

GPUInfo.org 简介

Vulkan 硬件数据库(GPUInfo.org)是 Vulkan 开发者不可或缺的资源。这个由社区驱动的数据库收集并展示了大量 GPU 和设备对 Vulkan 支持的信息。

GPUInfo.org 提供以下详细信息:

  • 支持的 Vulkan 版本

  • 可用的扩展

  • 功能支持

  • 实现限制

  • 格式属性

  • 队列族属性

这些信息由运行 Vulkan 硬件能力查看器工具的用户众包而来,该工具将 GPU 的能力报告给数据库。

使用 GPUInfo.org 进行开发

在开发 Vulkan 应用程序时,GPUInfo.org 可以帮助你:

  1. 确定最低要求:了解需要针对的 Vulkan 版本和扩展,以支持所需的硬件范围。

  2. 检查功能可用性:验证特定功能(如动态渲染、时间线信号量或光线追踪)是否被广泛支持。

  3. 识别实现限制:发现不同硬件上各种 Vulkan 功能的实际限制。

  4. 比较厂商和设备:了解 NVIDIA、AMD、Intel 和移动 GPU 厂商之间 Vulkan 支持的差异。

让我们看一些使用 GPUInfo.org 的实际示例:

示例:检查 Vulkan 版本支持

要确定 Vulkan 1.3(引入动态渲染)的支持广泛程度:

  1. 访问 GPUInfo.org

  2. 导航到“Core Version Support”

  3. 查看支持 Vulkan 1.3 的设备百分比

你会发现,虽然较新的 GPU 支持 Vulkan 1.3+,但仍有大量设备仅限于 Vulkan 1.0、1.1 或 1.2。

示例:检查扩展支持

如果你正在考虑使用某个特定扩展:

  1. 访问“Extensions”部分

  2. 搜索你感兴趣的扩展

  3. 查看不同厂商的支持百分比

这有助于你决定是否需要该扩展,或者提供回退路径。

示例:使用 Vulkan Configurator 工具

Vulkan Configurator 工具(在所有平台上可执行文件名为 vkconfig)包含在 Vulkan SDK 中,提供了一种方便的方式来配置系统上的 Vulkan 设置。使用方法如下:

  1. 启动 Vulkan Configurator

    • 在 Windows 上,建议从“开始”菜单启动“Vulkan Configurator”,因为从命令行运行 vkconfig.exe 仅显示有限选项

    • 在其他平台上,打开终端并运行 vkconfig

      注意,在所有平台上可执行文件都叫 `vkconfig`。
  2. 配置验证层

    • 导航到“Layers”选项卡

    • 根据调试需求启用或禁用特定的验证层

    • 例如,在开发期间启用 VK_LAYER_KHRONOS_validation 以捕获 API 使用错误

  3. 管理环境变量

    • 转到“Settings”选项卡

    • 设置环境变量,如 VK_LAYER_PATHVK_ICD_FILENAMES

    • 这些设置可以应用于整个系统或仅当前会话

  4. 配置驱动特定选项

    • 某些 GPU 厂商提供额外的配置选项

    • 可以通过特定于厂商的选项卡访问

  5. 导出配置

    • 保存配置以供以后使用或与团队成员共享

    • 这确保了不同开发机器上 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);
}

虽然这种方法有效,但它有几个缺点:

  1. 需要修改和重新编译代码来启用/禁用验证

  2. 更难尝试不同的验证层配置

  3. 增加了代码库的复杂性

更好的方法是使用 Vulkan Configurator 外部管理验证层。以下是修改代码以利用此优势的方法:

void createInstance() {
    // 应用程序信息...

    // 在不显式启用验证层的情况下创建实例
    vk::InstanceCreateInfo createInfo{
        .pApplicationInfo        = &appInfo,
        // ... 其他参数
    };

    instance = vk::raii::Instance(context, createInfo);
}

使用这种方法:

  1. 从应用程序中移除所有特定于验证层的代码

  2. 使用 Vulkan Configurator 在需要时启用验证层

  3. 无需重新编译即可切换验证配置

使用 Vulkan Configurator 启用验证层:

  1. 启动 Vulkan Configurator(在 Windows 上从开始菜单启动,或从终端运行 vkconfig - 可执行文件在所有平台上都叫 vkconfig

  2. 转到“Layers”选项卡

  3. 启用 VK_LAYER_KHRONOS_validation

  4. 应用设置

此配置将应用于在该环境中运行的所有 Vulkan 应用程序,使得无需更改代码即可轻松切换验证开/关。

这种方法的优点包括:

  • 更清晰的代码:应用程序代码无需处理验证层

  • 灵活性:无需重新编译即可更改验证设置

  • 一致性:在多个应用程序之间应用相同的验证设置

  • 实验性:轻松尝试不同的验证配置

其他有用的生态工具

除 GPUInfo.org 外,还有其他工具可以帮助你开发和调试 Vulkan 应用程序:

  • Vulkan SDK 工具

    • vulkaninfo:显示本地系统的 Vulkan 能力

    • vkconfig(Vulkan Configurator):一个用于管理 Vulkan 设置的配置工具(详见 [示例:使用 Vulkan Configurator 工具]

    • 验证层:帮助识别 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 {
    // 在旧版本上检查扩展
    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 应用程序的最佳实践:

  1. 在运行时检查功能可用性:不要仅根据 Vulkan 版本假设功能可用。始终检查特定功能和扩展。

  2. 提供回退路径:为现代功能不可用时实现替代代码路径。

  3. 使用功能结构体:在创建逻辑设备时,使用适当的功能结构体只启用你需要且可用的功能。

  4. 在各种硬件上测试:使用 GPUInfo.org 识别常见硬件配置,并在代表性样本上测试你的应用程序。

  5. 优雅降级:设计你的应用程序在功能较弱的硬件上运行时优雅地降低视觉质量或功能。

  6. 记录需求:清晰记录你的应用程序的最低和推荐 Vulkan 版本及扩展要求。

结论

理解 Vulkan 生态工具并知道如何为不同的 GPU 能力适配代码,是 Vulkan 开发者必备的技能。通过遵循本章中概述的方法,你可以创建在各种硬件上运行的应用程序,同时仍然在可用时利用最新特性。

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。