简述
Framebuffers, 帧缓冲区。之前我们在学习’Swap Chain-交换链’时提到Vulkan没有“默认帧缓冲区”的概念,取而代之的是名为 “swap chain” 即交换链,也就是渲染的缓冲区,必须在Vulkan中明确创建。 现在我们已经设置了渲染过程,以期望使用与交换链图像相同格式的单个帧缓冲区,但实际上我们还没有创建任何帧缓冲区。
通过将渲染过程创建期间指定的附件包装到VkFramebuffer对象中来绑定附件。
帧缓冲区对象引用表示附件的所有VkImageView对象。在我们的情况下,这将只是一个单一的:颜色附件。但是,我们必须用于附件的图像取决于在检索用于表示的图像时交换链返回的图像。这意味着我们必须为交换链中的所有图像创建一个帧缓冲区,并使用与绘制时检索到的图像相对应的帧缓冲区。
一. VkFramebuffer 创建帧缓冲区
帧缓冲区由VkFramebuffer句柄表示: VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkFramebuffer)
通过调用vkCreateFramebuffer来创建:
1 | VkResult vkCreateFramebuffer( |
- device: 创建帧缓冲区的逻辑设备
- pCreateInfo: 指向VkFramebufferCreateInfo结构,该结构描述有关帧缓冲区创建的附加信息。
- pAllocator: 控制主机内存分配
- pFramebuffer: 指向返回结果帧缓冲区对象的VkFramebuffer句柄。
我们创建一个VkFramebuffer的集合来保存所有帧缓冲:
1 | std::vector<VkFramebuffer> swapChainFramebuffers; |
在一个新函数createFramebuffers中为该数组创建对象,该函数在创建图形管道之后立即从initVulkan调用:
1 | void initVulkan() { |
在createFramebuffers最开始,调整帧缓冲区容器的大小以容纳所有交换链图像视图, 然后遍历图像视图并从它们创建帧缓冲区:
1 | void createFramebuffers() { |
如上,帧缓冲区的创建非常简单:
- 首先需要指定帧缓冲区需要与哪个renderPass兼容。只能对与之兼容的渲染过程使用帧缓冲区,这意味着它们使用相同数量和类型的附件。
- attachmentCount和pAttachments参数指定应绑定到渲染过程pAttachment数组中相应附件描述的VkImageView对象。
- 宽度和高度参数是交换链中获取的宽高
- layers是指图像数组中的层数。我们的交换链图像是单个图像,因此层的数量是1。
我们应该先删除帧缓冲区,然后再删除它们所基于的图像视图和渲染过程,但必须在完成渲染之后:
1 | void cleanup() { |
现在我们已经到达了一个里程碑–拥有了渲染所需的所有对象。接下来,我们将编写第一个实际的绘图指令。
二. Command buffers 指令缓冲区
Vulkan中的指令,比如绘图操作和内存传输,不是直接使用函数调用来执行的。必须在指令缓冲区对象中记录想要执行的所有操作。这样做的好处是,所有设置绘图指令的工作都可以提前在多个线程中完成。之后,只需告诉Vulkan在主循环中执行指令。
指令缓冲区是用来记录指令的对象,这些指令可以随后提交到设备队列中执行。指令缓冲区有两级:一级指令缓冲区,它可以执行二级指令缓冲区,并提交给队列;二级指令缓冲区,它可以由一级指令缓冲区执行,但不直接提交给队列。
2.1 Command pools 指令池
在创建指令缓冲区之前必须先创建指令池。指令池管理用于存储缓冲区的内存,并从它们中分配指令缓冲区。
指令池通过允许不同的线程使用不同的分配器来提高多线程性能,而不需要每次使用都进行内部同步。
由VkCommandPool对象表示: VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkCommandPool)
1 | VkCommandPool commandPool; |
创建指令池函数:
1 | VkResult vkCreateCommandPool( |
- device: 创建指令池的逻辑设备
- pCreateInfo: 一个指向VkCommandPoolCreateInfo结构实例的指针,该结构指定指令池对象的状态。
- pAllocator: 控制内存分配
- pCommandPool: 指向一个VkCommandPool句柄,创建的池返回该句柄。
2.1.1 创建指令池
我们创建一个新的函数:createCommandPool, 来执行创建指令池:
1 | void initVulkan() { |
指令缓冲区是通过将它们提交到设备队列(比如我们检索到的图形和展示队列)上来执行的。
每个指令池只能分配提交到单一类型队列上的指令缓冲区。我们将记录绘图的指令,这就是我们选择图形队列家族的原因。
2.1.2 VkCommandPoolCreateInfo
1 | typedef struct VkCommandPoolCreateInfo { |
- sType: 此结构的类型,VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO
- pNext: 为空或指向特定于扩展的结构的指针。
- flags: 是VkCommandPoolCreateFlagBits的位掩码, 指示指令池和从中分配的指令缓冲区的使用行为。
- queueFamilyIndex: 指定队列族。从这个指令池分配的所有指令缓冲区必须在来自相同队列族的队列上提交。
2.1.3 VkCommandPoolCreateFlags
1 | typedef enum VkCommandPoolCreateFlagBits { |
- VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:指定从池中分配的指令缓冲区将是短暂的,这意味着它们将在相对较短的时间内被重置或释放。这个标志可以被实现用来控制池内的内存分配行为。
- VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许从池中分配的任何指令缓冲区被单独重置到初始状态;或者通过调用vkResetCommandBuffer,或者在调用vkBeginCommandBuffer时通过隐式重置。如果在池中没有设置这个标志,那么vkResetCommandBuffer绝对不能被从池中分配的任何指令缓冲区调用。
- VK_COMMAND_POOL_CREATE_PROTECTED_BIT:指定从池中分配的指令缓冲区为受保护的指令缓冲区。如果未启用受保护的内存特性,则不能设置VK_COMMAND_POOL_CREATE_PROTECTED_BIT标志位。
鉴于我们将只在程序开始时记录指令缓冲区,然后在主循环中多次执行它们,因此我们不会使用这这些标志。
2.1.4 vkDestroyCommandPool
使用vkCreateCommandPool功能完成指令池的创建,指令将在整个程序中使用来绘制屏幕上的东西,所以池应该只在结束时销毁:
1 | void cleanup() { |
2.2 Command buffer allocation 指令缓冲区分配
现在我们可以开始分配指令缓冲区并在其中记录绘图指令。因为其中一个绘图指令涉及到绑定正确的VkFramebuffer,所以我们实际上必须再次为交换链中的每个图像记录一个指令缓冲区。
为此,创建一个VkCommandBuffer对象列表作为类成员。当它们的指令池被销毁时,指令缓冲区将被自动释放,所以我们不需要显式的清理。
1 | std::vector<VkCommandBuffer> commandBuffers; |
现在开始使用createCommandBuffers函数,为每个交换链图像(VkImageView)分配和记录指令。
1 | void initVulkan() { |
2.2.1 vkAllocateCommandBuffers
指令缓冲区是通过vkAllocateCommandBuffers函数分配的,该函数以VkCommandBufferAllocateInfo结构体作为参数,指定指令池和要分配的缓冲区数量:
1 | VkResult vkAllocateCommandBuffers( |
- device: 指令池所属的逻辑设备。
- pAllocateInfo: 指向VkCommandBufferAllocateInfo结构实例的指针,该结构描述了分配的参数。
- pCommandBuffers: 指向VkCommandBuffer句柄数组的指针,在该数组中返回生成的指令缓冲区对象。数组必须至少为pAllocateInfo的commandBufferCount成员指定的长度commandBufferCount。每个分配的指令缓冲区都从初始状态开始。(就是commandBuffers集的首个元素的地址)
2.2.2 VkCommandBufferAllocateInfo
1 | typedef struct VkCommandBufferAllocateInfo { |
- sType: 结构体类型
- pNext: 为空或指向特定于扩展的结构的指针
- commandPool: 分配此指令缓冲区的指令池
- level: VkCommandBufferLevel值,指定指令缓冲区级别
- commandBufferCount: 从池中分配的指令缓冲区的数量
VkCommandBufferLevel, 指定指令缓冲区级别:
1 | typedef enum VkCommandBufferLevel { |
- VK_COMMAND_BUFFER_LEVEL_PRIMARY:指定主指令缓冲区,可以提交到队列执行,但不能从其他指令缓冲区调用。
- VK_COMMAND_BUFFER_LEVEL_SECONDARY:指定次要指令缓冲区,不能直接提交,但可以从主指令缓冲区调用。
2.2.3 createCommandBuffers
接下来就是完成指令缓冲区集的创建:
1 | void createCommandBuffers() { |
在这里不使用辅助指令缓冲区功能,但是可以想象,重用来自主要指令缓冲区的常用操作是很有帮助的
2.3 启动指令缓冲区记录
记录的指令包括将管道和描述符集绑定到指令缓冲区的指令、修改动态状态的指令、绘制指令(用于图形渲染)、调度指令(用于计算)、执行次要指令缓冲区的指令(仅用于主要指令缓冲区)、复制缓冲区和图像的指令,以及其他指令。
为什么需要记录指令? 先看一下指令缓冲区的生命周期。
2.3.1 指令缓冲区的生命周期
每个指令缓冲区总是处于以下状态之一:
- Initial(初始状态): 当一个指令缓冲区被分配时,它处于初始状态。一些指令能够将一个指令缓冲区或一组指令缓冲区从任何可执行状态、记录状态或无效状态重置回该状态。处于初始状态的指令缓冲区只能移动到记录状态或释放。
- Recording(记录状态): vkBeginCommandBuffer改变指令缓冲区的状态从初始状态到记录状态。一旦指令缓冲区处于记录状态,可以使用vkCmd*指令记录到指令缓冲区。
- Executable(可执行状态): vkEndCommandBuffer结束指令缓冲区的记录,并将其从记录状态移动到可执行状态。可执行指令缓冲区可以提交、重置或记录到另一个指令缓冲区。
- Pending(挂起状态): 指令缓冲区的队列提交将指令缓冲区的状态从可执行状态更改为挂起状态。在挂起状态下,应用程序不能试图以任何方式修改指令缓冲区-因为设备可能正在处理记录到它的指令。一旦指令缓冲区的执行完成,指令缓冲区将返回到可执行状态,或者返回无效状态(如果它是通过VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT记录的)。应该使用一个同步指令来检测何时发生这种情况。
- Invalid(无效状态):某些操作,如修改或删除记录到指令缓冲区的指令中使用的资源,将把该指令缓冲区的状态转换为无效状态。处于无效状态的指令缓冲区只能被重置或释放。
所以记录状态可以理解为记录绘制操作,然后记录完毕就转为可执行状态,等待被执行。
2.3.2 记录指令缓冲区
通过调用vkBeginCommandBuffer开始记录指令缓冲区,并使用一个小的VkCommandBufferBeginInfo结构作为参数,指定关于这个特定指令缓冲区使用的一些细节。
1 | VkResult vkBeginCommandBuffer( |
- commandBuffer: 待放入记录状态的指令缓冲区的句柄。
- pBeginInfo: VkCommandBufferBeginInfo结构的一个实例,它定义了关于指令缓冲区如何开始记录的附加信息。
1 | typedef struct VkCommandBufferBeginInfo { |
- sType: 结构体类型
- pNext: 为空或指向特定于扩展的结构的指针
- flags: 是VkCommandBufferUsageFlagBits的位掩码,指定命令缓冲区的使用行为。
- pInheritanceInfo: 是一个指向VkCommandBufferInheritanceInfo结构的指针,如果commandBuffer是一个次要命令缓冲区,就会使用这个结构。如果这是一个主命令缓冲区,那么这个值将被忽略。
在VkCommandBufferBeginInfo::flags中设置位来指定命令缓冲区的使用行为:
1 | typedef enum VkCommandBufferUsageFlagBits { |
- VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:指定命令缓冲区的每个记录将只提交一次,命令缓冲区将被重置并在每次提交之间再次记录。
- VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:指定次要命令缓冲区被认为完全位于渲染通道内。如果这是一个主命令缓冲区,那么这个位会被忽略。
- VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:指定当命令缓冲区处于挂起状态时,可以重新提交给队列,并记录到多个主要命令缓冲区中。
开始指令缓冲区记录:
1 | for (size_t i = 0; i < commandBuffers.size(); i++) { |
我们使用VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT标志是因为我们可能已经在为下一帧调度绘图命令,而最后一帧还没有完成。pInheritanceInfo参数仅与辅助命令缓冲区相关。它指定从调用的主命令缓冲区继承哪个状态。
如果命令缓冲区已经被记录过一次,那么调用vkBeginCommandBuffer将隐式重置它。以后不可能向缓冲区追加命令。
2.4 启动渲染通道
通过vkCmdBeginRenderPass开始绘制。渲染通道是使用VkRenderPassBeginInfo结构中的一些参数来配置的。
1 | void vkCmdBeginRenderPass( |
- commandBuffer: 指令缓冲区
- VkRenderPassBeginInfo: 是一个指向VkRenderPassBeginInfo结构的指针,提供的渲染通道的详细信息
- contents: 是一个VkSubpassContents值,控制渲染通道内的绘图命令将如何提供,可以有两个值:
- VK_SUBPASS_CONTENTS_INLINE:render pass命令将被嵌入到主命令缓冲区中,而不会执行次要命令缓冲区。
- VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS:render pass命令将从次要命令缓冲区执行。
在开始渲染通道实例之后,命令缓冲区准备好记录该渲染通道的第一个子通道的命令。
2.4.1 VkRenderPassBeginInfo
1 | typedef struct VkRenderPassBeginInfo { |
- sType: 结构体类型
- pNext: 为空或指向特定于扩展的结构的指针
- renderPass: 渲染通道
- framebuffer: 指令缓冲区
- renderArea: 渲染通道实例影响的渲染区域
- clearValueCount: pClearValues中的元素数量
- pClearValues: 是一个VkClearValue结构的数组,它包含每个附件的清除值,如果附件使用的loadOp值是VK_ATTACHMENT_LOAD_OP_CLEAR,或者附件具有深度/模板格式并使用的stencilLoadOp值是VK_ATTACHMENT_LOAD_OP_CLEAR。数组按附件号索引。只使用与已清除附件对应的元素。pClearValues的其他元素将被忽略。
renderArea渲染区域是受渲染通道实例影响的渲染区域。附件加载、存储和多样本解析操作的影响仅限于x和y坐标落在所有附件渲染区域内的像素。渲染区域扩展到framebuffer的所有层。应用程序必须确保(必要时使用scissor)所有的渲染都包含在渲染区域内。渲染区域必须包含在framebuffer尺寸内。
2.4.2 start a Render Pass
应用程序一次记录一个render pass实例的命令,方法是开始一个render pass实例,遍历子通道(subpass)来记录该子通道的命令,然后结束render pass实例。
1 | VkRenderPassBeginInfo renderPassInfo = {}; |
2.4.3 基本绘制指令
1 | // 渲染通道现在可以开始了。所有记录命令的函数都可以通过它们的vkCmd前缀来识别。它们都返回void,所以在完成记录之前不会有错误处理。 |
2.4.3.1 vkCmdBindPipeline
管道被创建后,可以使用以下命令将其绑定到指令缓冲区:
1 | void vkCmdBindPipeline( |
- commandBuffer: 是即将绑定到管道的命令缓冲区
- pipelineBindPoint:是一个VkPipelineBindPoint值,指定管道对象是图形管道还是计算管道。
- VK_PIPELINE_BIND_POINT_COMPUTE: 管道控制vkCmdDispatch和vkCmdDispatchIndirect的行为。
- VK_PIPELINE_BIND_POINT_GRAPHICS: 管道控制所有绘制命令的行为。其他命令不受管道状态的影响。
- pipeline: 即将绑定的管道
一旦绑定,管道绑定将影响命令缓冲区中的后续图形或计算命令,直到另一个管道绑定到绑定点。
2.4.3.2 vkCmdDraw
1 | void vkCmdDraw( |
- commandBuffer: 绘制指令记录到的指令缓冲区
- vertexCount: 要绘制的顶点数
- instanceCount: 要绘制的实例数
- firstVertex: 要绘制的第一个顶点的索引
- firstInstance: 要绘制的第一个实例的索引
当执行该命令时,将使用当前基元拓扑和vertexCount连续顶点索引(第一个vertexIndex值等于firstVertex)组装基元。这些原语是用从firstInstance开始的instanceIndex绘制的instanceCount时间,并按顺序增加每个实例。组装的原语执行绑定的图形管道。
2.4.3.3 vkCmdEndRenderPass
在记录了最后一个子过程的指令之后,结束渲染通道实例调用:
1 | void vkCmdEndRenderPass( |
结束渲染通道实例在最终子通道上执行一切多样本解析操作。
2.4.3.4 vkEndCommandBuffer
一旦开始记录,应用程序将记录指令序列(vkCmd*),以在指令缓冲区、绘制、调度和其他指令中设置状态。调用完成vkEndCommandBuffer命令缓冲区的记录:
1 | VkResult vkEndCommandBuffer( |
如果在记录过程中发生了错误,vkEndCommandBuffer将返回一个不成功的返回码来通知应用程序。
如果应用程序希望进一步使用指令缓冲区,则必须重置指令缓冲区。指令缓冲区必须处于记录状态,并被移动到可执行状态。