
Vulkan的一些基本概念。
基本概念
Instance
App与vulkan之间的桥梁,创建一个Instance即创建一个vulkan对象,之后才可以进行更多操作。
Physical Device
物理设备,通常来说是一个显卡(GPU),也可以是其他设备,如NPU、DSP等。
Device
相当于一个在App中的physical device实例,App可以创建多个Device与物理设备交互和管理资源;类比OpenGL,相等于Context。
QueueFamily
每一个物理设备的功能不同,如显卡有计算和图形渲染功能,而NPU只能计算,DSP可以解码,这些功能都由物理设备的QueueFamily体现,每一种功能对应一种QueueFamily。
App与vulkan交互的逻辑就是,通过QueueFamily创建处对应的Queue,然后将Command推入Queue中,vulkan会把这些command交给设备进行处理。
Queue
Queue是vulkan很重要的概念,可以理解为App与设备之间通信的队列。
每种Queue可以接受不同的Command,取决于它属于哪一个QueueFamily。
在创建Queue时,指定对应的family的index,便可以创建属于该family的Queue。
Image、Buffer
都属于内存空间对象。不同的是Image表示特定用途的对象,如颜色缓冲,纹理,深度等。Buffer仅仅代表原始数据缓存。
Memory
一块内存。可以是CPU内存,也可以是GPU内存(显存)。创建一个Buffer或Image这样的内存对象,必须给他们分配内存,即Memory。
ImageView
Image具有明确的用途,需要让vulkan知道如何使用Image。
因此要告诉vulkan这个Image的类型格式等属性,ImageView用以描述这些属性,vulkan不会直接操作Image,而是通过ImageView对象来操作Iamge。
CommandBuffer
命令缓冲,
CommandPool
用来分配CommandBuffer。由于每一个Queue支持的Command不同(用途不同),在创建对应Queue的CommandPool时,也应该指定与创建Queue时一样的QueueFamily的Index。
Pipeline
图形渲染管线,与OpenGL中的概念相似。顶点着色器->细分着色器->几何着色器->光栅化着色器->片段着色器。OpenGL中固有这些,而vulkan需要手动创建。(也因此可以看出vulkan的可编程的程度极深)
Framebuffer
帧缓冲,即渲染目标对象。Framebuffer需要关联不同的Image,这些Image起到不同的作用,颜色缓冲、深度缓冲、模板缓冲等。
RenderPass
可以理解为Pipeline的输入参数,最终要的输入参数是Framebuffer。
Discriptor
Discriptor是单个资源的抽象句柄(如纹理、缓冲)。
DiscriptorSet是一组描述符集合,对应着色器中的set绑定组。
DescriptorPool(描述符池) 是用于高效分配和管理 描述符集(DescriptorSet) 的核心对象。它类似于内存池的概念,主要作用是通过预分配资源减少动态内存分配的开销,从而提升渲染性能。
Descriptor预先分配一定数量和类型的描述符(如UniformBuffer、StorageBuffer、纹理、采样器等),后续从池中分配描述符集,避免频繁的全局内存操作和同步开销。
1)通过描述符集布局定义资源绑定规则;2)从描述符池分配描述符集;3)更新描述符集指向的资源;4)绑定描述符集到管线供着色器使用。
Submit
CommandBuffer不能直接推入Queue中,因为多个CommandBuffer可能需要进行同步、排他等设置,因此需要将CommandBuffer和同步、排他对象一起放到同一个Submit对象中,然后将Submit推入Queue中。这样vulkan可以根据Submit中的对象来安排CommandBuffer的执行顺序。
常用的控制CommandBuffer之间、CPU-GPU、GPU-GPU之间的同步或排他对象由VkEvent、VkSemaphore和VkFence。
UniformBuffer/StorageBuffer
这两种buffer都是用于着色器与CPU之间传递的结构化数据。
UniformBuffer是一种只读缓冲,用于传递全局、低频更新的数据(如MVP矩阵、灯光参数等)。绘制期间,在着色器中无法修改。需要显式对齐,std140.
StorageBuffer是一种可读写缓冲,用于传递大量结构化数据(如顶点数据、粒子状态、计算着色器结果)。支持动态修改和原子操作。对齐灵活,std340.
特性 | Uniform Buffer | Storage Buffer |
---|---|---|
访问权限 | 只读 | 可读写(支持原子操作) |
数据规模 | 适合小数据(KB 级) | 适合大数据(MB/GB 级) |
内存布局 | std140 (严格对齐) |
std430 (更灵活对齐) |
性能优化 | 低延迟,专用缓存 | 高带宽,适合批量操作 |
典型用途 | 矩阵、全局参数 | 粒子系统、计算着色器、通用数据结构 |
Vulkan 标志 | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT |
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
内存布局规则
Uniform Buffer(std140)
基础类型对齐:标量(如
float
)按 4 字节对齐,向量按 4N 字节对齐(如vec3
对齐到 16 字节)。
结构体内存:成员按声明顺序对齐,整体大小填充为 16 的倍数。
// C++ 结构体(需手动填充)
struct UniformBufferObject {
alignas(16) glm::mat4 model; // 16 字节对齐
alignas(16) glm::mat4 view;
alignas(16) glm::mat4 proj;
};
Storage Buffer(std430)
更宽松对齐:标量和向量按自然大小对齐(如
vec3
对齐到 12 字节)。数组支持:动态数组无需预定义大小(在着色器中声明为
positions[]
// C++ 结构体(无需严格填充)
struct Particle {
glm::vec4 position; // 16 字节对齐
glm::vec4 velocity; // 自然对齐
};
ComputeShader
属于计算管线的核心部分,它不处理顶点、片段或几何数据,而是通过工作组并行执行用户定义的计算任务。无固定输入输出,完全由开发者定义,通过工作组和线程手动划分并行任务,无需绑定RenderPass或FrameBuffer(独立于渲染管线)。
一般用于预处理或后处理,可以与图形管线无缝协作。可以用于图片模糊、锐化、粒子系统、光线追踪、几何处理、科学计算等。
渲染器
双缓冲命令队列
维护两个命令队列,一个队列用于接收新的渲染命令,另一个队列用于执行渲染命令,通过SwapQueues
在两个队列之间切换。这种设计可以实现渲染命令的并行处理:当GPU执行一个队列命令时,CPU可以同时向另一个队列中提交新的命令。
Placement new
Placement new是C++中的一种特殊的new操作符,它允许我们在已分配的内存空间中构造对象。与普通的new操作符不同,placement new不分配内存,只在指定的内存位置上调用构造函数。
可以在实时系统中避免动态内存分配。
不需要使用delete,因为内存不是new分配的。
void* memory = // 某个内存位置 |
vkDeviceWaitIdle
该函数会阻塞 CPU 线程,直到设备关联的所有队列(图形、计算、传输等)全部完成当前提交的任务并进入空闲状态。
函数 | 作用范围 | 使用场景 |
---|---|---|
vkQueueWaitIdle |
单个指定队列 | 等待特定队列任务完成(如图形队列) |
vkDeviceWaitIdle |
设备所有队列 | 全局同步或设备级资源管理 |
vkFence |
异步信号通知 | 非阻塞式等待特定任务完成 |
渲染流程
- 创建Instance对象
- 通过Intance列举所有Physical Device
- 从Physical Device中挑选一个使用,并创建其Device对象
- 通过Device对象创建支持图形指令的Queue
- 通过Device创建Pipeline
- 创建Shader
- 创建RenderPass
- 创建Framebuffer
- 创建对应的Image,分配内存
- 通过Device创建CommandPool
- 通过CommandPool分配CommandBuffer
- 主循环
- 使用渲染命令填充CommandBuffer
- 将填充后的CommandBuffer和排他、同步对象封装到Submit中
- 将Submit推入Queue
- 回收资源
渲染命令的提交
这是一种封装,将所有与渲染相关的任务封装成一个函数(命令),提交到队列中,队列维护一个头指针和动指针,和一个buffer,此buffer中存储提交的函数指针。
后面在渲染进程中执行的就是此buffer(队列)中的命令(指针所指的函数)。
// 提交渲染命令,FuncT为函数类型,使用std::forward<FuncT>(func),可以完美转发FuncT类型的函数 |
双缓冲渲染命令队列
最开始是一个空渲染帧,最开始当然什么命令也没有,执行0个命令,然后开始主循环。
循环最开始是先执行渲染队列0中已提交的命令,第一个主循环当然依然没有命令,此时主线程同时向渲染队列1中提交命令,渲染线程渲染完毕后等待,主线程提交命令后进入下一循环。
之后的循环:主线程在渲染线程渲染完毕后开始进入下一帧,切换命令队列,唤醒渲染线程渲染上一个队列的命令,同时提交当前队列的命令;依然是渲染线程渲染完毕后阻塞等待,主线程提交完毕后进入下一循环。
单线程模式下: 主线程提交命令与渲染线程执行命令不会同时进行,而是先将上一帧的渲染完成,然后再提交命令,如此往复。
[11:14:20] [53480] [info] [MainThread] SwapQueues: 0 |
渲染线程的终止
当程序终止,要妥善处理线程的终止,避免死锁导致渲染进程Join后无法退出的问题。
对比以下个log:
[14:16:22] [15200] [warning] ======== Begin Loop ======== |
[14:17:54] [47488] [warning] ======== Begin Loop ======== |
这里有一个潜在的问题,如果在m_IsRunning被设置为false之后,渲染线程先退出循环,这里再进行Pump,就会导致主线程无限等待渲染结束,而渲染线程则永远也无法结束(因为根本无法开始————循环已经结束了)
解决方法是先等待渲染线程空闲,开启下一帧,Kick渲染使其开始渲染,此时立刻关闭循环,并入主线程,等待渲染线程的结束,主线程就可以顺利结束了
三缓冲渲染(多帧并行)
vkFence
栅栏是一种同步原语,可用于将依赖项从队列插入到主机。栅栏有两种状态 - 已发出信号和未发出信号。栅栏可在执行队列提交命令时发出信号。可以使用 vkResetFences 在主机上取消栅栏信号。主机可以使用 vkWaitForFences 命令等待栅栏,并且可以使用 vkGetFenceStatus 查询当前状态。
vkWaitForFences
VkResult vkWaitForFences( |
调用后CPU阻塞,直到GPU完成上一帧的工作并发出Fence信号或者达到超时时间。
如果GPU处理速度跟不上CPU提交速度,会导致CPU等待,这可能会影响整体性能,通常通过使用多个”frame in flight”(多帧并行)来减少等待,使用多个交替的Fence来实现CPU与GPU的并行工作。
// 等待上一帧完成 |
多帧并行
// 每个frame都有独立的同步对象 |
与双缓冲命令队列不同,命令队列只关注渲染命令的记录(CPU中)和执行(GPU中),而FramesInFlight要管理所有帧相关的资源。
KHR后缀
在Vulkan中,带有KHR后缀和不带KHR后缀的函数主要区别在于:
扩展与核心功能:
vkWaitSemaphoresKHR
: 是KHR扩展的一部分,最初作为扩展功能引入vkWaitSemaphores
: 是Vulkan核心功能的一部分,在后续版本中被纳入标准
可用性:
- KHR版本需要检查设备是否支持该扩展
- 非KHR版本在支持该版本的Vulkan实现中直接可用
使用方式:
// KHR扩展版本需要先启用扩展
VkDeviceCreateInfo createInfo = {};
createInfo.ppEnabledExtensionNames = { "VK_KHR_timeline_semaphore" };
// 核心版本直接使用
vkWaitSemaphores(device, ...);版本要求:
- KHR版本通常在较早的Vulkan版本中就可用
- 非KHR版本需要更新的Vulkan版本
一般建议:
- 如果需要支持较老的Vulkan版本,使用KHR版本
- 如果使用较新的Vulkan版本,优先使用非KHR版本
- 有时可能需要根据运行时检测来决定使用哪个版本
注意:这个规则适用于Vulkan中所有带KHR后缀和不带KHR后缀的函数对。KHR表示这个功能最初是由Khronos Group(Vulkan的开发组织)作为扩展引入的。
描述符集
描述符池,Vulkan中的描述符集不能直接创建,只能从特定的缓冲池中分配,这个缓冲池就是描述符池,在创建描述符池的时候需要分配描述符池的大小,里面各描述符集的数量。
描述符集就是描述符的集合,Set 就是 DataSet 就是数据集。
描述符用以关联用户的数据资源和着色器,如Uniform、Sampler、Texture等,为了让VulkanAPI识别资源,引入描述符和描述符集布局。描述符布局绑定就是定义绑定关系,如绑定到着色器layout(binding = 0)上。描述符是一种通信协议,用于CPU与着色器通信。在系统内部,描述符提供了一种静默的机制,通过位置绑定的方式来关联资源内存与着色器。
Vulkan渲染管线
共三种,ComputePipeline和GraphicsPipeline和Ray tracing pipeline。Pipeline中包含了PipelineLayout,PipelineLayout制定了管线使用的那些DescriptoSetLayout和PushConstant。
计算管线(Compute Pipeline)是比较简单的一种,因为它所支持的都是只计算的程序(称为Computer Shader)。
GraphicsPipeline要复杂得多如下图所示,因为它包含了所有的参数,如顶点、片段、几何、计算和曲面细分着色器(如果启用了该功能的话),再加上像顶点属性、输入装配、图元结构、光栅化参数、深度/模板测试参数、面剔除和Blend Const、视口(ViewPort)参数还有RenderPass等,Vulkan会将这些状态固定成一个大的、不可更改的对象。
- 本文标题:Vulkan基本概念
- 创建时间:2025-02-21 20:54:00
- 本文链接:2025/02/21/note/Programming/CG/Vulkan/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!