这个系列文章我们来介绍一位海外工程师如何探索 Vulkan 音视频技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,本篇介绍 Vulkan 基础库组件。
——来自公众号“关键帧Keyframe”的分享
SaschaWillems/Vulkan 仓库提供了一套全面的基础库组件,作为所有 Vulkan 示例的基石。这些组件抽象了常见的 Vulkan 操作,减少了样板代码,并为 Vulkan 应用开发提供了结构化的方法。在本文中,我们将探讨该基础库的关键组件,并了解它们如何协同工作以简化 Vulkan 编程。
基础库的核心是 VulkanExampleBase 类,它作为本仓库中所有示例的基础。该类封装了完整的 Vulkan 初始化管线,并为构建 Vulkan 应用提供了一个框架,只需最少的设置。
class VulkanExampleBase
{
// Vulkan 核心对象
VkInstance instance{ VK_NULL_HANDLE };
VkPhysicalDevice physicalDevice{ VK_NULL_HANDLE };
VkDevice device{ VK_NULL_HANDLE };
VkQueue queue{ VK_NULL_HANDLE };
// 交换链和呈现
VulkanSwapChain swapChain;
// 命令管理
VkCommandPool cmdPool{ VK_NULL_HANDLE };
std::vector<VkCommandBuffer> drawCmdBuffers;
// 同步
struct {
VkSemaphore presentComplete;
VkSemaphore renderComplete;
} semaphores{};
std::vector<VkFence> waitFences;
// 渲染资源
VkRenderPass renderPass{ VK_NULL_HANDLE };
std::vector<VkFramebuffer> frameBuffers;
// 封装的设备
vks::VulkanDevice *vulkanDevice{};
};
VulkanExampleBase 类处理完整的 Vulkan 初始化序列,从实例创建到交换链设置和命令缓冲区管理。它提供了一个结构化的生命周期,派生类可以接入该生命周期以实现特定的渲染逻辑。
来源:vulkanexamplebase.h#L78-L178
1、使用 VulkanDevice 进行设备管理
vks::VulkanDevice 类封装了物理和逻辑 Vulkan 设备,为设备操作提供了简化的接口。这个包装类处理设备初始化、队列管理和内存操作。
namespace vks
{
struct VulkanDevice
{
VkPhysicalDevice physicalDevice;
VkDevice logicalDevice;
VkPhysicalDeviceProperties properties;
VkPhysicalDeviceFeatures features;
VkPhysicalDeviceMemoryProperties memoryProperties;
struct {
uint32_t graphics;
uint32_t compute;
uint32_t transfer;
} queueFamilyIndices;
operator VkDevice() const { return logicalDevice; }
uint32_t getMemoryType(uint32_t typeBits, VkMemoryPropertyFlags properties, VkBool32 *memTypeFound = nullptr) const;
VkResult createLogicalDevice(VkPhysicalDeviceFeatures enabledFeatures, std::vector<const char *> enabledExtensions, void *pNextChain, bool useSwapChain = true, VkQueueFlags requestedQueueTypes = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT);
VkResult createBuffer(VkBufferUsageFlags usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, VkDeviceSize size, VkBuffer *buffer, VkDeviceMemory *memory, void *data = nullptr);
};
}
VulkanDevice 类简化了几项复杂操作:
- 设备选择和创建:它处理选择合适的物理设备,并创建具有所需功能和扩展的逻辑设备。
- 内存管理:
getMemoryType()方法帮助根据使用要求找到正确的内存类型以进行缓冲区分配。 - 缓冲区创建:
createBuffer()方法提供了一个简化的接口,用于创建缓冲区并自动分配内存和可选的数据初始化。
来源:VulkanDevice.h#L22-L69
2、使用 VulkanSwapChain 进行交换链管理
VulkanSwapChain 类抽象了跨不同平台的交换链创建和管理的复杂性。它处理平台特定的表面创建,并为交换链操作提供统一接口。
class VulkanSwapChain
{
private:
VkInstance instance{ VK_NULL_HANDLE };
VkDevice device{ VK_NULL_HANDLE };
VkPhysicalDevice physicalDevice{ VK_NULL_HANDLE };
VkSurfaceKHR surface{ VK_NULL_HANDLE };
public:
VkFormat colorFormat{};
VkColorSpaceKHR colorSpace{};
VkSwapchainKHR swapChain{ VK_NULL_HANDLE };
std::vector<VkImage> images{};
std::vector<VkImageView> imageViews{};
// 平台特定的表面初始化
#if defined(VK_USE_PLATFORM_WIN32_KHR)
void initSurface(void* platformHandle, void* platformWindow);
#elif defined(VK_USE_PLATFORM_ANDROID_KHR)
void initSurface(ANativeWindow* window);
// ... 其他平台定义
void create(uint32_t& width, uint32_t& height, bool vsync = false, bool fullscreen = false);
VkResult acquireNextImage(VkSemaphore presentCompleteSemaphore, uint32_t& imageIndex);
VkResult queuePresent(VkQueue queue, uint32_t imageIndex, VkSemaphore waitSemaphore = VK_NULL_HANDLE);
};
VulkanSwapChain 类提供了几个关键优势:
- 平台抽象:它处理 Windows、Android、Linux(XCB 和 Wayland)、macOS、iOS 和其他平台的平台特定表面创建。
- 简化的交换链创建:
create()方法处理交换链创建的复杂过程,并带有用于垂直同步和全屏模式的参数。 - 图像获取和呈现:
acquireNextImage()和queuePresent()方法简化了帧呈现过程。
来源:VulkanSwapChain.h#L30-L101
3、使用 VulkanBuffer 进行缓冲区管理
vks::Buffer 类封装了 Vulkan 缓冲区对象及其关联内存,为缓冲区操作提供了简化接口。
namespace vks
{
struct Buffer
{
VkDevice device;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDescriptorBufferInfo descriptor;
VkDeviceSize size = 0;
VkDeviceSize alignment = 0;
void* mapped = nullptr;
VkBufferUsageFlags usageFlags;
VkMemoryPropertyFlags memoryPropertyFlags;
VkResult map(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
void unmap();
VkResult bind(VkDeviceSize offset = 0);
void setupDescriptor(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
void copyTo(void* data, VkDeviceSize size);
VkResult flush(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
VkResult invalidate(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
void destroy();
};
}
Buffer 类通过以下方式简化缓冲区管理:
- 封装缓冲区和内存:它将 Vulkan 缓冲区和设备内存对象组合成一个实体。
- 内存映射:
map()和unmap()方法提供了对缓冲区内存的便捷访问以进行数据传输。 - 数据操作:
copyTo()、flush()和invalidate()方法简化了数据传输和缓存管理。 - 描述符设置:
setupDescriptor()方法准备缓冲区以供描述符集使用。
来源:VulkanBuffer.h#L24-L45
VulkanTools 命名空间提供了一系列实用函数,简化了常见的 Vulkan 操作和错误处理。
// 检查并显示 Vulkan 返回结果的宏
#define VK_CHECK_RESULT(f) \
{ \
VkResult res = (f); \
if (res != VK_SUCCESS) \
{ \
LOGE("Fatal : VkResult is \" %s \" in %s at line %d", vks::tools::errorString(res).c_str(), __FILE__, __LINE__); \
assert(res == VK_SUCCESS); \
} \
}
VK_CHECK_RESULT 宏特别有用,因为它自动检查 Vulkan 返回代码,并在出现问题时提供详细的错误消息。这有助于调试,并确保 Vulkan 错误在开发过程的早期被发现。
VulkanTools 命名空间中的其他实用函数包括:
- 从文件加载着色器
- 创建图像视图
- 转换图像布局
- 设置图像内存屏障
- 错误字符串转换以进行调试
来源:VulkanTools.h#L40-L50
4、组件交互
基础库组件以协调的方式协同工作,以提供完整的 Vulkan 应用框架。下图说明了主要组件之间的关系:
使用此基础库的 Vulkan 应用中的典型操作流程是:
- 初始化:
VulkanExampleBase类初始化 Vulkan 实例,选择物理设备,通过vks::VulkanDevice创建逻辑设备,并设置交换链。 - 资源创建:应用使用基础库提供的实用函数和类创建缓冲区、纹理和其他资源。
- 渲染循环:应用进入主渲染循环,在该循环中获取交换链图像,记录命令缓冲区,将它们提交到队列,并呈现结果。
- 清理:当应用关闭时,基础库处理所有 Vulkan 资源的正确清理。
5、示例用法
以下是一个简化示例,展示了这些组件在典型 Vulkan 应用中如何协同工作:
class MyVulkanExample : public VulkanExampleBase
{
public:
vks::Buffer vertexBuffer;
vks::Buffer indexBuffer;
void prepare() override
{
// 创建顶点缓冲区
vulkanDevice->createBuffer(
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
&vertexBuffer,
vertexBufferSize,
vertexData);
// 创建索引缓冲区
vulkanDevice->createBuffer(
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
&indexBuffer,
indexBufferSize,
indexData);
// 设置描述符、管线等
}
void render() override
{
// 获取下一帧图像
VK_CHECK_RESULT(swapChain.acquireNextImage(semaphores.presentComplete, currentBuffer));
// 提交命令缓冲区
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE));
// 呈现帧
VK_CHECK_RESULT(swapChain.queuePresent(queue, currentBuffer, semaphores.renderComplete));
}
};
此示例演示了基础库组件如何通过将复杂操作抽象为易于使用的类和方法来简化 Vulkan 应用开发。
来源:vulkanexamplebase.h#L78-L178, VulkanDevice.h#L57-L59, VulkanSwapChain.h#L88-L98
6、其他组件
除了我们讨论的核心组件外,基础库还包括其他几个有用的类和实用程序:
6.1、VulkanTexture
VulkanTexture 类封装了 Vulkan 图像对象及其关联的视图和采样器,为纹理操作提供了简化接口。
6.2、VulkanUIOverlay
VulkanUIOverlay 类将 Dear ImGui 库与 Vulkan 集成,允许在 Vulkan 应用中轻松创建用户界面。
6.3、相机和数学实用程序
基础库包括相机类(camera.hpp)和数学实用程序,这些实用程序简化了常见的 3D 图形操作,如视图和投影矩阵计算。
6.4、glTF 模型加载
VulkanglTFModel 类提供了在 Vulkan 应用中加载和渲染 glTF 3D 模型的功能。
7、结论
SaschaWillems/Vulkan 仓库中的基础库组件为 Vulkan 应用开发提供了全面的框架。通过抽象常见操作并为 Vulkan 编程提供结构化方法,这些组件显著降低了创建 Vulkan 应用所需的复杂性和样板代码。
无论您是 Vulkan 新手还是经验丰富的开发者,这些组件都可以作为您自己项目的基础,或作为理解 Vulkan 最佳实践的参考。模块化设计允许您仅使用所需的组件,使该库灵活且适应不同的项目需求。
学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

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