这个系列文章我们来介绍一位海外工程师如何探索 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)
此方法处理整个缓冲区创建过程:
- 创建缓冲区句柄
- 分配和绑定内存
- (可选)将初始数据复制到缓冲区
这显著减少了缓冲区创建所需的样板代码,这是 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 开发过程。
学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

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