探索 Vulkan 音视频技术(3):设备管理

这个系列文章我们来介绍一位海外工程师如何探索 Vulkan 音视频技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,本篇介绍 Vulkan 设备管理。

——来自公众号“关键帧Keyframe”的分享

在 Vulkan 中,设备管理是一个基础概念,它架起了应用程序与物理 GPU 之间的桥梁。SaschaWillems/Vulkan 仓库通过其 VulkanDevice 类提供了一种健壮且结构良好的方法来处理物理和逻辑设备。让我们探讨这个关键组件的工作原理,以及如何在你的 Vulkan 应用程序中利用它。

在深入实现之前,必须理解 Vulkan 中物理和逻辑设备之间的区别:

  • 物理设备:表示系统中的实际 GPU 硬件。每个物理设备都具有应用程序可以查询的特定功能、限制和属性。
  • 逻辑设备:表示应用程序与物理设备的连接。正是通过逻辑设备,你才能创建缓冲区、纹理和命令缓冲区等资源。

VulkanDevice 类优雅地封装了这两个概念,为设备管理提供了统一接口。

来源:base/VulkanDevice.h#L24-L27

1、VulkanDevice 类架构

VulkanDevice 类旨在简化设备管理,同时保持灵活性。以下是其核心结构:

struct VulkanDevice
{
    VkPhysicalDevice physicalDevice;      // 物理设备表示
    VkDevice logicalDevice;              // 逻辑设备表示
    VkPhysicalDeviceProperties properties; // 设备属性和限制
    VkPhysicalDeviceFeatures features;    // 可用设备功能
    VkPhysicalDeviceFeatures enabledFeatures; // 已启用的功能
    VkPhysicalDeviceMemoryProperties memoryProperties; // 内存属性
    std::vector<VkQueueFamilyProperties> queueFamilyProperties; // 队列族
    std::vector<std::string> supportedExtensions; // 支持的扩展
    // ... 队列族索引和其他成员
};

此结构存储了应用程序生命周期中所需的关于设备的所有基本信息。

来源:base/VulkanDevice.h#L24-L48

2、设备初始化过程

设备初始化过程遵循一个清晰的序列,从选择物理设备开始,到创建具有所需功能和扩展的逻辑设备结束。

2.1、物理设备选择

初始化从 VulkanExampleBase 的 initVulkan() 方法开始,该方法枚举可用的物理设备并选择一个:

// 获取可用物理设备的数量
uint32_t gpuCount = 0;
VK_CHECK_RESULT(vkEnumeratePhysicalDevices(instance, &gpuCount, nullptr));

// 枚举设备
std::vector<VkPhysicalDevice> physicalDevices(gpuCount);
result = vkEnumeratePhysicalDevices(instance, &gpuCount, physicalDevices.data());

// 选择物理设备(默认选择第一个设备)
physicalDevice = physicalDevices[selectedDevice];

该框架还支持通过命令行参数选择 GPU,这对于具有多个 GPU 的系统非常有用。

来源:base/vulkanexamplebase.cpp#L1054-L1098

2.2、创建 VulkanDevice 实例

选择物理设备后,框架会创建一个 VulkanDevice 实例:

vulkanDevice = new vks::VulkanDevice(physicalDevice);

构造函数会自动查询并存储设备属性、功能、内存属性和支持的扩展:

VulkanDevice::VulkanDevice(VkPhysicalDevice physicalDevice)
{
    this->physicalDevice = physicalDevice;

    // 存储属性、功能、限制和属性
    vkGetPhysicalDeviceProperties(physicalDevice, &properties);
    vkGetPhysicalDeviceFeatures(physicalDevice, &features);
    vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memoryProperties);

    // 获取队列族属性
    uint32_t queueFamilyCount;
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
    queueFamilyProperties.resize(queueFamilyCount);
    vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyProperties.data());

    // 获取支持的扩展列表
    uint32_t extCount = 0;
    vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extCount, nullptr);
    // ... 存储扩展
}

这种自动信息收集可让您免于在应用程序中反复查询相同信息。

来源:base/VulkanDevice.cpp#L25-L58

2.3、 创建逻辑设备

最后一步是创建具有所需功能和扩展的逻辑设备:

result = vulkanDevice->createLogicalDevice(enabledFeatures, enabledDeviceExtensions, deviceCreatepNextChain);

createLogicalDevice 方法处理以下复杂过程:

  • 设置用于图形、计算和传输操作的队列族
  • 启用设备功能
  • 启用扩展
  • 使用适当配置创建逻辑设备

来源:base/vulkanexamplebase.cpp#L1116-L1121

3、队列族管理

Vulkan 设备管理中最复杂的方面之一是处理队列族。不同的队列族支持不同类型的操作(图形、计算、传输),并非所有设备都为每种操作类型提供单独的队列。

VulkanDevice 类通过 getQueueFamilyIndex 方法简化了这一点,该方法智能地为给定操作选择最佳队列族:

uint32_t VulkanDevice::getQueueFamilyIndex(VkQueueFlags queueFlags) const
{
    // 尝试找到专用的计算队列(无图形支持)
    if ((queueFlags & VK_QUEUE_COMPUTE_BIT) == queueFlags) {
        for (uint32_t i = 0; i < queueFamilyProperties.size(); i++) {
            if ((queueFamilyProperties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) &&
                ((queueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0)) {
                return i;
            }
        }
    }

    // 尝试找到专用的传输队列(无图形/计算支持)
    if ((queueFlags & VK_QUEUE_TRANSFER_BIT) == queueFlags) {
        for (uint32_t i = 0; i < queueFamilyProperties.size(); i++) {
            if ((queueFamilyProperties[i].queueFlags & VK_QUEUE_TRANSFER_BIT) &&
                ((queueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) &&
                ((queueFamilyProperties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0)) {
                return i;
            }
        }
    }

    // 对于其他队列类型,返回支持请求标志的第一个
    for (uint32_t i = 0; i < queueFamilyProperties.size(); i++) {
        if ((queueFamilyProperties[i].queueFlags & queueFlags) == queueFlags) {
            return i;
        }
    }

    throwstd::runtime_error("Could not find a matching queue family index");
}

此方法在可用时优先选择专用队列,这可以通过允许操作在不同队列族上并行运行来提高性能。

来源:base/VulkanDevice.cpp#L127-L165

4、内存管理

内存管理是 Vulkan 设备处理的另一个关键方面。VulkanDevice 类提供了 getMemoryType 方法,用于为资源找到适当的内存类型:

uint32_t VulkanDevice::getMemoryType(uint32_t typeBits, VkMemoryPropertyFlags properties, VkBool32 *memTypeFound) const
{
    for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++) {
        if ((typeBits & 1) == 1) {
            if ((memoryProperties.memoryTypes[i].propertyFlags & properties) == properties) {
                if (memTypeFound) {
                    *memTypeFound = true;
                }
                return i;
            }
        }
        typeBits >>= 1;
    }

    if (memTypeFound) {
        *memTypeFound = false;
        return0;
    } else {
        throwstd::runtime_error("Could not find a matching memory type");
    }
}

创建缓冲区和图像时,此方法至关重要,因为它可帮助您找到满足特定要求的内存(例如,用于最佳性能的设备本地内存,用于 CPU 访问的主机可见内存)。

来源:base/VulkanDevice.cpp#L88-L115

5、扩展支持检查

现代 Vulkan 应用程序通常依赖扩展来实现高级功能。VulkanDevice 类使检查扩展支持变得容易:

bool VulkanDevice::extensionSupported(std::string extension)
{
    return (std::find(supportedExtensions.begin(), supportedExtensions.end(), extension) != supportedExtensions.end());
}

这个简单的方法允许您在尝试使用所需扩展之前验证它们是否可用,从而防止运行时错误。

来源:base/VulkanDevice.cpp#L553-L556

6、缓冲区创建助手

VulkanDevice 类提供了用于创建缓冲区的便捷方法,这是 Vulkan 应用程序中的基本资源:

VkResult VulkanDevice::createBuffer(VkBufferUsageFlags usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, VkDeviceSize size, VkBuffer *buffer, VkDeviceMemory *memory, void *data = nullptr)

此方法处理整个缓冲区创建过程:

  1. 创建缓冲区句柄
  2. 分配和绑定内存
  3. (可选)将初始数据复制到缓冲区

这显著减少了缓冲区创建所需的样板代码,这是 Vulkan 应用程序中的常见操作。

来源:base/VulkanDevice.cpp#L319-L364

7、命令缓冲区管理

VulkanDevice 类还提供了用于命令缓冲区管理的助手方法:

VkCommandBuffer VulkanDevice::createCommandBuffer(VkCommandBufferLevel level, bool begin = false)
void VulkanDevice::flushCommandBuffer(VkCommandBuffer commandBuffer, VkQueue queue, bool free = true)

这些方法简化了命令缓冲区的创建和提交,这对于记录和执行 GPU 命令至关重要。

来源:base/VulkanDevice.cpp#L498-L501, base/VulkanDevice.cpp#L541-L544

8、在应用程序中使用 VulkanDevice

以下是在应用程序中使用 VulkanDevice 类的典型工作流程:

// 1. 创建 VulkanDevice 实例
vulkanDevice = new vks::VulkanDevice(physicalDevice);

// 2. 启用所需功能和扩展
VkPhysicalDeviceFeatures enabledFeatures{};
enabledFeatures.fillModeNonSolid = VK_TRUE; // 示例:启用线框渲染

std::vector<constchar*> enabledExtensions;
enabledExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);

// 3. 创建逻辑设备
VkResult result = vulkanDevice->createLogicalDevice(enabledFeatures, enabledExtensions, nullptr);

// 4. 使用设备创建资源
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
vulkanDevice->createBuffer(
    VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
    bufferSize,
    &vertexBuffer,
    &vertexBufferMemory,
    vertexData
);

// 5. 使用设备进行命令缓冲区操作
VkCommandBuffer cmdBuffer = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
// ... 记录命令
vulkanDevice->flushCommandBuffer(cmdBuffer, queue);

9、设备选择注意事项

使用多个 GPU 时,您可能希望实现更复杂的设备选择逻辑。该框架通过公开设备属性为此提供了基础:

// 检查设备属性以做出明智的选择
VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties);

// 考虑设备类型(独立、集成等)
if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
    // 这是独立显卡
}

// 检查设备限制
if (deviceProperties.limits.maxImageDimension2D > 4096) {
    // 设备支持大纹理
}

来源:base/vulkanexamplebase.cpp#L1101-L1103

10、清理和资源管理

正确的清理在 Vulkan 中对于避免资源泄漏至关重要。VulkanDevice 析构函数会自动处理此问题:

VulkanDevice::~VulkanDevice()
{
    if (commandPool) {
        vkDestroyCommandPool(logicalDevice, commandPool, nullptr);
    }
    if (logicalDevice) {
        vkDestroyDevice(logicalDevice, nullptr);
    }
}

当应用程序关闭时,只需删除 VulkanDevice 实例,它就会清理所有关联的资源。

来源:base/VulkanDevice.cpp#L65-L75

11、结论

SaschaWillems/Vulkan 仓库中的 VulkanDevice 类为 Vulkan 设备管理提供了全面的解决方案。它在抽象掉大部分复杂性的同时,仍让您完全控制设备配置。通过封装物理设备属性、逻辑设备创建、队列管理和资源创建助手,它可作为 Vulkan 应用程序的绝佳基础。

无论您是构建简单的三角形渲染器还是复杂的游戏引擎,理解并有效使用此设备管理系统都将显著简化您的 Vulkan 开发过程。

学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

探索 Vulkan 音视频技术(3):设备管理

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论