![第 9 章. Drawing Objects 绘制对象](https://i-blog.csdnimg.cn/blog_migrate/b22576e956f18f9350e721dcd10eaea1.png)
第 9 章. Drawing Objects 绘制对象
第 9 章. Drawing Objects 绘制对象
在上两章中,我们实现了渲染通道实例并显示了具有指定背景色的交换链图像。 在前一章中,我们将各种管线状态与 Render Pass 一起放入图形管线中。 在本章中,我们将把所有以前的实现结合在一起来构建 Vulkan 中的第一个 Hello World!程序并在显示输出中显示我们的第一个渲染对象。
本章全面介绍了在 Vulkan 中绘制对象的过程;包括记录和执行绘图对象的命令缓冲区。 记录将 Render Pass,framebuffer 和管线与视口和几何数据相关联。 命令缓冲区执行涉及在设备队列中提交命令缓冲区,并将绘制的交换链图像呈现在展示引擎上。 在本章的最后,我们将讨论 Vulkan API 中可用的各种同步原语。
在本章中,我们将介绍以下主题:
- Vulkan 中绘图过程概述
- 准备绘图对象
- 渲染绘图对象
- 渲染索引化的几何图形
- 了解 Vulkan 中的同步原语
- 调整显示窗口的大小
Vulkan 中绘图过程概述
在 Vulkan 中实现绘图对象很简单;它由两个阶段组成:准备或构建绘图对象并对其进行渲染。 前一阶段产生命令缓冲区并记录绘图命令。 在后一阶段,命令缓冲区会执行这些绘图命令来渲染对象。 我们来仔细看看这些阶段:
- 准备绘图对象:此阶段生成绘制对象所需的命令缓冲区:
- 命令缓冲区创建:Command buffer creation,对于每个交换链彩色图像,需要创建一个相应的命令缓冲区对象。 例如,如果交换链是双缓冲区,那么它将包含两个彩色图像。 因此,我们应该创建两个命令缓冲区对应于每个图像。
- 命令缓冲区记录:Command buffer recording,对于创建的命令缓冲区,一次记录 Render Pass 实例命令的一个子通道,请参阅以下步骤:
- 将创建的 Render Pass 对象和 framebuffer 与展示渲染区域所需的尺寸关联。
- 指定清除背景色和深度图的标量值。 我们在第 7 章“缓冲区资源”,“渲染通道”,“帧缓冲区”和“使用 SPIR-V 的着色器”中实现了此步骤。 有关更多信息,请参阅“清除背景色”部分。
- 绑定图形管线对象 graphics pipeline object。
- 绑定管线使用的资源,包括顶点缓冲区 vertex buffers 和描述符集 descriptor sets。
- 定义视口 viewport 和剪裁区域 scissoring region。
- 绘制对象。
- 渲染绘图对象:Rendering the drawing object,在这里,准备阶段中创建的命令缓冲区会被多次重用和执行,以便渲染绘图对象:
- 获取(对它来说)可用于执行渲染的交换链图像 swapchain image。
- 提交命令缓冲区 command buffer 到渲染对象 render the object。
- 在展示引擎 presentation engine 上显示渲染的图形图像 rendered drawing image。
可绘制对象的准备和渲染是通过 VulkanDrawable 类的 prepare()和 render()函数实现的。
头文件声明
在本节中,我们先来看看 VulkanDrawable 的头文件声明。 请按照内联注释了解每个函数和变量的功能和用途。 在我们继续阅读本章时,我们会实现这些函数并对其详细讨论。 以下是新增的成员变量和函数:
class VulkanDrawable
{
public:
// Prepares the drawing object before rendering,
// allocate, create, record command buffer
void prepare();
// Renders the drawing object
void render();
// Initialize the viewport parameters here
void initViewports(VkCommandBuffer* cmd);
// Initialize the scissor parameters here
void initScissors(VkCommandBuffer* cmd);
// Destroy the drawing command buffer object
void destroyCommandBuffer(); private:
// Command buffer for drawing
std::vector<VkCommandBuffer> vecCmdDraw;
// Prepares render pass instance
void recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw);
// Viewport and Scissor variables
VkViewport viewport; VkRect2D scissor;
VkSemaphore presentCompleteSemaphore; VkSemaphore drawingCompleteSemaphore;
};
prepare()函数创建命令缓冲区对象,这些对象会在 render()函数中用于绘制对象。 prepare()函数从命令池(VulkanRenderer :: cmdPool)为命令缓冲区(vecCmdDraw)分配内存并创建命令缓冲区对象。 命令缓冲区的命令记录在 recordCommandBuffer()函数内部;这会创建 Render Pass 实例和其他一些重要的作业,例如将图形管线与绘图对象相关联,并通过 initViewport()和 initScissor()指定视口和裁剪管理。
render()函数使用准备好的记录的命令缓冲区,并在可用的交换链彩色图像上渲染绘图图形。 一旦在交换链的彩色图像上完成绘图,就会将其交给展示引擎用于显示目的。
准备绘图对象 drawing object
绘图对象的准备工作在 prepare()函数中实现;第 7 章中已经介绍了该函数 ------“缓冲区资源,渲染通道,帧缓冲区以及使用 SPIR-V 的着色器”。 有关更多信息,请参阅“清除背景色”中的“在 Render Pass 实例中设置背景色”小节。
记录 Render Pass 命令
Render Pass 实例一次记录命令的一个子通道。 一个渲染通道可能包含一个或多个子通道。 对于每个子通道,都使用 vkCmdBeginRenderPass()和 vkCmdEndRenderPass()API 记录这些命令。 这两个 API 共同定义了一个范围,在这个范围内会迭代通过不同的子通道,它们会记录特定于该子通道的命令。
开始 Render Pass 记录
vkCmdBeginRenderPass()API 会为给定的子通道开始 Render Pass 实例命令的记录。 以下是规范:
void vkCmdBeginRenderPass(
VkCommandBuffer commandBuffer,
const VkRenderPassBeginInfo* pRenderPassBegin,
VkSubpassContents contents);
vkCmdBeginRenderPass API 接受三个参数。 VkCommandBuffer 类型的第一个参数 commandBuffer 指示把命令记录到其中的命令缓冲区。 第二个参数 pRenderPassBegin 是传递 Render Pass 元数据到其中的 VkRenderPassBeginInfo 类型的控制结构(更多信息在下一节中介绍)。 最后一个参数是 VkSubpassContents 类型,并指明将内容记录在子通道执行过程中的什么位置以及以何种方式存储。
以下是 VkSubpassContents 的两种类型:
- K_SUBPASS_CONTENTS_INLINE:在这种类型中,主命令缓冲区直接记录子通道内容,并且不允许在该子通道内执行辅助命令缓冲区。
- VK_SUBPASS_CONTENTS_SECONDARY_COMMAND:在这里,辅助命令缓冲区通过主命令缓冲区被调用并负责记录辅助通道的内容。
提示
主命令缓冲区和辅助命令缓冲区是什么??primary and secondary command buffers
主命令缓冲区没有任何父命令缓冲区。 而辅助命令缓冲区总是从主命令缓冲区执行,相当于它的父亲。 辅助命令缓冲区不直接被提交到设备队列中;这些会被记录到主命令缓冲区中并使用 vkCmdExecuteCommands()API 执行。 void vkCmdExecuteCommands(VkCommandBuffer commandBuffer,uint32_t commandBufferCount,const VkCommandBuffer * pCommandBuffers); 这个 API 有三个参数。 第一个参数 -commandBuffer(类型为 Vk CommandBuffer) - 是主命令缓冲区对象的句柄。 第二个参数 commandBufferCount(类型为 uint32_t) - 表示需要在主命令缓冲区下调用的辅助命令缓冲区的总数。 最后一个参数 -pCommandBuffers(类型为 VkCommandBuffer ) - 指定要传入的辅助命令缓冲区对象的完整列表。
辅助命令缓冲区如何有用?
辅助命令缓冲区在将常用的操作记录到模块单元中时很有用。 这些模块化的组件可以根据需要附加到任何您想要的主缓冲区。 在没有辅助命令缓冲区的情况下,这些常用操作会被记录为主缓冲区的一部分,使它们体积庞大并导致冗余,造成污染。 Let’s take a look at the VkRenderPassBeginInfo structure syntax and its parameters 我们来看看 VkRenderPassBeginInfo 结构的语法及其参数:
typedef struct VkRenderPassBeginInfo {
VkStructureType sType;
const void pNext;
VkRenderPass renderPass;
VkFramebuffer framebuffer;
VkRect2D renderArea;
uint32_t clearValueCount; const VkClearValue* pClearValues;
} VkRenderPassBeginInfo;
以下是 VkRenderPassBeginInfo 结构的各种字段:
字段 | 描述
—|---
sType | 这是这个控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO。
pNext | 这可能是一个指向扩展特定结构的有效指针,也可能是 NULL。
renderPass | 渲染通道实例使用渲染通道对象来开始记录。
framebuffer | 渲染通道还与包含颜色和深度附件的 VkFramebuffer 对象关联。 renderArea | 该字段表示由于渲染通道执行而受影响的矩形区域。
clearValueCount | 这表示颜色或深度的清除值的数量。
pClearValues | 该字段包含与帧缓冲区 framebuffer 中指定的附件关联的清除值。
注意
在 Render Pass 实例中定义 renderArea 时,如果渲染区域小于 framebuffer,则可能会导致性能损失。 在这种情况下,建议保持渲染区域等于帧缓冲区域,或者限定渲染通道 Render Pass 的粒度。 渲染区域的粒度可以使用 vkGetRenderAreaGranularity()API 进行检查。
void vkGetRenderAreaGranularity( VkDevice device,
VkRenderPass renderPass,
VkExtent2D* pGranularity);
该 API 接受三个参数:第一个参数 device 是与 renderPass 关联的 VkDevice; 第二个参数是 renderPass 对象;最后一个参数用 pGranularity 检索粒度大小。
过渡到下一个子通道 Transitioning to the next subpass
在给定的渲染通道实例中,当子通道记录完成时,应用程序可以使用 vkCmdNextSubpass()API 切换或过渡到下一个子通道。
void vkCmdNextSubpass( VkCommandBuffer commandBuffer, VkSubpassContents contents);
该 API 接受两个参数,如下表所示:
参数 | 描述
—|---
commandBuffer | 这表示将命令记录到其中的命令缓冲区。
contents | 这表明内容将在何处以及如何在下一个子通道执行过程中提供。 有关 VkSubpassContents 的更多信息,请参阅上一小节 - 开始 Render Pass 实例。
完成渲染通道 Render Pass 实例的记录
vkCmdEndRenderPass()API 结束当前正在被执行的子通道的 Render Pass 实例命令缓冲区的记录。 该 API 使用一个参数来指定必须停止记录的命令缓冲区的句柄。
void vkCmdEndRenderPass( VkCommandBuffer commandBuffer);
实现
当前的子通道是用纯黑色指定的,会用这个值绘制交换链图像,使背景显示为黑色。 另外,还指定了其他参数,例如渲染通道对象,帧缓冲区和尺寸。 在 Render Pass 实例中有很多其他命令被执行;这将在下一节讨论,下面的实现显示了使用 vkCmdBeginRenderPass()和 vkCmdEndRenderPass()API 的 Render Pass 实例的记录过程:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw)
{
VulkanDevice* deviceObj= rendererObj->getDevice();
// Specify the clear color value VkClearValue clearValues[2]; clearValues[0].color.float32[0]= 0.0f; clearValues[0].color.float32[1]= 0.0f; clearValues[0].color.float32[2]= 0.0f; clearValues[0].color.float32[3]= 0.0f;
// Specify the depth/stencil clear value clearValues[1].depthStencil.depth = 1.0f; clearValues[1].depthStencil.stencil = 0;
// Define the VkRenderPassBeginInfo control structure VkRenderPassBeginInfo renderPassBegin; renderPassBegin.sType =
VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassBegin.pNext = NULL;
renderPassBegin.renderPass = rendererObj->renderPass;
renderPassBegin.framebuffer = rendererObj-> framebuffers[currentImage];
renderPassBegin.renderArea.offset.x = 0;
renderPassBegin.renderArea.offset.y = 0; renderPassBegin.renderArea.extent.width = rendererObj->width; renderPassBegin.renderArea.extent.height= rendererObj->height; renderPassBegin.clearValueCount = 2;
renderPassBegin.pClearValues = clearValues;
// Start recording the render pass instance
vkCmdBeginRenderPass(*cmdDraw, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE);
// Execute the commands as per requirement
. . . .
// pipeline bind, geometry, viewport, scissoring
// End of render pass instance recording
vkCmdEndRenderPass(cmdDraw);
. . . .
}
Binding pipeline object 绑定管线对象
在 Render Pass 实例中,我们需要做的第一件事是使用 vkCmdBindPipeline()API 绑定管线。 此 API 将一个特定的管线(图形或计算)与使用此命令的当前命令缓冲区进行绑定。
void vkCmdBindPipeline(
VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline);
参数 | 描述
—|---
commandBuffer | 这表示将被绑定到管线 pipeline 对象的命令缓冲区对象(用于 Render Pass 记录操作)。
pipelineBindPoint | 该参数表示管线对象将被绑定到的管线绑定点的类型。 该参数的类型为 VkPipelineBindPoint,可以采用以下两个给定值之一:
typedef enum VkPipelineBindPoint { VK_PIPELINE_BIND_POINT_GRAPHICS = 0, VK_PIPELINE_BIND_POINT_COMPUTE = 1, } VkPipelineBindPoint;
第一个值表示用于图形管线的绑定点,第二个表示用于计算管线的绑定点。
pipeline | 这个参数表示命令缓冲区将被绑定到的管线对象。
每个管线 — 图形或计算 — 对于那些一旦绑定就会受到其影响的命令来说是非常特定的:
- 计算管线:Compute pipeline,当管线绑定到 VK_PIPELINE_BIND_POINT_COMPUTE 时,只能控制 vkCmdDispatch 和 vkCmdDispatchIndirect 的命令行为。 此管线状态下的其他任何命令都不会受影响。 有关这些命令的更多信息,请参阅官方的 Vulkan 规范文档 https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html。
- 图形管线:Graphics pipeline,当管线绑定到 VK_PIPELINE_BIND_POINT_GRAPHICS 时,可以控制 vkCmdDraw,vkCmdDrawIndexed,vkCmdDrawIndirect 和 vkCmdDrawIndexedIndirect 命令。 此管线状态下的其他任何命令都不会受到影响。
Implementation 实现
以下代码显示了图形管线与现有命令缓冲区对象 cmdDraw 的绑定过程,管线对象 pipeline 与图形绑定点(VK_PIPELINE_BIND_POINT_GRAPHICS)进行连接:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer cmdDraw){
. . . .
// Bound the command buffer with the graphics pipeline
vkCmdBindPipeline(cmdDraw, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipeline);
. . . .
}
注意
有关管线创建和实现中指定的管线对象的更多信息,请参阅第 8 章“管线和管线状态管理”。
指定绘图对象的几何信息 drawing object geometry information
绘图对象的几何信息可以使用顶点缓冲区来指定。 请记住,我们在第 7 章“缓冲区资源,渲染通道,帧缓冲区和使用 SPIR-V 的着色器”中构建了缓冲区资源,并将顶点信息存储在 VulkanDrawble 类的 VertexBuffer.buf 中。
注意
有关顶点缓冲区构建的更多信息和概述,请参阅第 7 章“缓冲区资源,渲染通道,帧缓冲区和使用 SPIR-V 的着色器”中的“了解缓冲区资源和使用缓冲区资源创建几何图形”部分。
顶点数据包含以交错形式存储的顶点位置和颜色信息。 使用 vkCmdBindVertexBuffers()API 将此顶点数据绑定到图形管线状态下的命令缓冲区。 该命令会对每个基本的绘图操作,将特定的顶点缓冲区绑定到命令缓冲区。
void vkCmdBindVertexBuffers(
VkCommandBuffer commandBuffer,
uint32_t firstBinding,
uint32_t bindingCount,
const VkBuffer pBuffers,
const VkDeviceSize pOffsets);
下表列出了各个字段并指出了每个参数的说明:
参数 | 描述
—|---
commandBuffer | 这是要将 vkCmdBindVertexBuffers 命令记录到其中的的命令缓冲区对象。
firstBinding | 该参数表示将由命令进行更新的顶点输入绑定的索引。 bindingCount | 这表示其状态将由命令进行更新的顶点输入绑定的数量。 pBuffers | 该参数是传入的 VkBuffer 顶点缓冲区句柄的数组。
pOffsets | 该参数是顶点缓冲区偏移量的数组。
Implementation 实现
vkCmdBindVertexBuffer()API 会使用必要的信息绑定命令缓冲区对象,从而选取几何数据。此 API 会获取我们感兴趣的绘图对象的顶点缓冲区信息,以便在显示器上进行绘制:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw){
. . . .
// Bound the vertex buffer with the command buffer vkCmdBindVertexBuffers(cmdDraw, 0, 1, &VertexBuffer.buf, offsets);
. . . .
}
定义动态视口 dynamic viewport
视口确定了绘图表面区域的部分范围,绘图对象图元会在其上进行渲染。 在第 8 章“管线和管线状态管理”中,我们在“了解管理状态对象(PSO)”下的“视口管理”小节中学习了在图形管线下管理视口的状态,并创建了管线状态对象。 视口参数可以进行静态或动态控制:
- 静态控制:Statical control,如果禁用了动态状态 VK_DYNAMIC_STATE_VIEWPORT,则一旦使用 VkPipelineViewportStateCreateInfo 类的成员变量 pViewport 创建视口管线状态对象,则无法支持更改和指定视口参数。
- 动态控制:Dynamical control,另一方面,当创建管线状态对象的时候启用了动态状态 VK_DYNAMIC_STATE_VIEWPORT 的情况下,就允许在运行时更改视口转换参数。 这些参数可以在运行时使用 vkCmdSetViewport()API 进行动态控制。 以下是此 API 的语法和描述:
void vkCmdSetViewport( VkCommandBuffer commandBuffer, uint32_t firstViewport,
uint32_t viewportCount, const VkViewport pViewports);
其中的每个参数的说明如下:
参数 | 描述
—|---
commandBuffer | 此参数指定要用来记录此命令的命令缓冲区对象。
firstViewport | firstViewport 是一个内部视口数组的索引,指示需要进行更新的第一个视口。
viewportCount | 这些是 pViewport 中视口的总数,pViewport 的参数将由命令进行更新。 pViewports | 这是 VkViewport 结构的指针数组,这个结构指定视口区域的尺寸。
typedef struct VkViewport { float x;
float y;
float width;
float height;
float minDepth;
float maxDepth;
} VkViewport;
其中的字段 说明如下:
字段 | 描述
—|---
x, y | 这是视口的左上角(x,y)。
width, height | 这表示视口的宽度和高度。 minDepth,
maxDepth | 这是视口的深度范围。 minDepth 可能大于或等于 maxDepth。
Implementation 实现
使用 initViewport()函数初始化视口,通过传递当前命令缓冲区对象(会把 vkCmdSetViewport()命令记录到其中)。 vkCmdSetViewport()API 设置视口参数,视口参数是在视口区域中动态设置的,指定可视区域的左上角位置和深度信息:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw){
. . . .
// Define the dynamic viewport here.
initViewports(cmdDraw);
. . . .
}
void VulkanDrawable::initViewports(VkCommandBuffer* cmd)
{
viewport.height = (float)rendererObj->height; viewport.width = (float)rendererObj->width; viewport.minDepth = (float) 0.0f; viewport.maxDepth = (float) 1.0f;
viewport.x = 0;
viewport.y = 0;
vkCmdSetViewport(cmd, 0, NUMBER_OF_VIEWPORTS, &viewport);
}
Scissoring 裁剪
裁剪器定义了一个矩形区域;位置(x,y)落在该矩形区域之外的所有帧缓冲区的片段都会丢弃。
如果不是在启用动态状态 VK_DYNAMIC_STATE_VIEWPORT 的情况下创建这个管线状态对象,则使用 VkPipelineViewportStateCreateInfo 类的成员变量 pScissors 控制裁剪矩形。 相反,如果在启用动态状态 VK_DYNAMIC_STATE_VIEWPORT 的情况下创建这个管线状态对象,则可以使用 vkCmdSetScissor()API 动态指定和控制裁剪矩形的参数。
与视口参数类似,裁剪器参数也可以进行静态或动态的控制:
- 静态控制:Static control,当不打算改变视口的参数时,则表示观看尺寸是固定的。 这可以通过禁用 VK_DYNAMIC_STATE_VIEWPORT 的方式,指示底层管线按照这样的形式使用动态状态。 这种情况下会通知管线视口相关的静态特性,这可能有利于做出决策并避免视口动态形式所需的多种操作。 对于静态的视口配置,会从视口管线状态对象 VkPipelineViewportStateCreateInfo 类的成员变量 pScissors 读取裁剪参数。
- 动态控制:Dynamical control,另一方面,如果启用了动态状态 VK_DYNAMIC_STATE_VIEWPORT,则可以通过称为 vkCmdSetViewport()的特殊 API 动态更改和指定裁剪参数。 以下是此 API 的语法和描述:
void vkCmdSetScissor( VkCommandBuffer commandBuffer, uint32_t firstScissor,
uint32_t scissorCount, const VkRect2D pScissors);
其中每个参数的说明如下:
参数 | 描述
—|---
commandBuffer | 此参数指定要用于记录此命令的命令缓冲区对象。
firstScissor | firstScissor 是一个内部裁剪器数组的索引,表示要更新的第一个裁剪器。
scissorCount | 这是 pScissors 数组中裁剪器的总数,其参数会由命令进行更新。
pScissors | 这是一个指向 VkRect2D 结构的数组的指针,这个结构指定裁剪区域的 2D 尺寸。
实现
通过使用指定非剪辑区域的 initScissors()函数初始化裁剪器。 这个矩形区域外的任何东西都会被丢弃。 vkCmdSetScissor()API 设置了一些裁剪参数,它们是可以动态设置的,指示用于单裁剪或多裁剪的矩形尺寸:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw){
. . . .
// Define the scissor here. initScissors(cmdDraw);
. . . .
}
void VulkanDrawable::initScissors(VkCommandBuffer* cmd)
{
scissor.extent.width = rendererObj->width; scissor.extent.height = rendererObj->height; scissor.offset.x = 0;
scissor.offset.y = 0;
vkCmdSetScissor(*cmd, 0, NUMBER_OF_SCISSORS, &scissor);
}
绘图命令 Draw command
绘图命令有助于组装图元。 Vulkan 支持基于索引和非索引的绘制命令。 绘图命令可以通过片段排列的顺序影响帧缓冲区。 在使用一个绘图命令的多个实例的情况下,API 的排序次序用来处理绘图命令。 对于基于非索引的命令,规则是将较低数量实例的图元排在靠前的位置,以此类推。 对于基于索引的命令,具有较少数量的顶点索引值的图元会排在 API 排序中靠前的位置,以此类推。
Vulkan 通过在命令缓冲区中记录绘图命令的形式渲染绘图对象。 在 Vulkan 中有四种不同的绘图命令,它们大致分为以下两类:
- 第一种类型(vkCmdDraw 和 vkCmdDrawIndexed)指定命令缓冲区对象本身的绘图参数。
- 相反,第二种类型(vkCmdDrawIndirect 和 vkCmdDrawIndexedIndirect)使用缓冲区内存从绘图 API 读取参数,该参数后缀为 Indirect 关键字,并且属于后者;否则,它就是前者。
vkCmdDraw 命令
vkCmdDraw()API 从命令缓冲区读取绘图参数;按照数组的形式,以特定的顺序访问顶点信息,从第一个顶点(firstVertex)开始,直到 vertexCount 指定的顶点总数。 vkCmdDraw API 使用顶点数组的数据信息渲染由管线状态对象中的输入装配状态指定的图元。
这个 API 支持实例化,允许多次高效渲染对象而无需调用多个绘制命令。 这些绘图功能在人群的模拟,树的渲染,克隆模式等情况下非常有用。 使用 instanceCount 指定绘图实例的总数,从 firstInstance 指示的第一个实例索引开始计算。
void vkCmdDraw(
VkCommandBuffer commandBuffer, uint32_t vertexCount,
uint32_t instanceCount,
uint32_t firstVertex,
uint32_t firstInstance);
其中每个参数的说明如下:
参数 | 描述
—|---
commandBuffer | 该参数指定执行记录的命令缓冲区对象(VkCommandBuffer)。 vertexCount | 这是要绘制的顶点的总数。
instanceCount | 该参数表示使用此命令要绘制的实例总数。
firstVertex | 该参数指定为了绘制所有顶点而从中读取的第一个顶点的索引。
firstInstance | 该参数指定要绘制实例的第一个实例 ID。
本章中的示例使用以下命令:
vkCmdDraw(*cmdDraw, 3, 1, 0, 0);
该命令用到了下面的三角形数据,以交错的形式表示三个顶点,因此第二个参数指定为 3; 由于只有一个实例,因此第三个参数为 1。绘图应从第一个顶点开始,第一个顶点由索引 0 指示,作为第四个参数。 最后一个参数是 0,指向第一个实例 ID:
struct VertexWithColor
{
float x, y, z, w; // Vertex Position
float r, g, b, a; // Color format Red, Green, Blue, Alpha
};
static const VertexWithColor triangleData[] =
{
};
注意
{ 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0 },/Vertex0/
{ 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0 },/Vertex1/
{-1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0 },/Vertex2/
本章中使用的另一种绘图命令是 vkCmdDrawIndexed()API。 此 API 用来渲染索引化的几何图形。 有关详细信息,请参阅本章末尾处的“渲染索引化的几何图形”小节。 但在跳到这节之前,您必须看完所有剩余的部分,以了解 Vulkan 中绘图对象的渲染。
实现绘图对象 drawing object 的准备工作
绘图对象的准备过程在 VulkanDrawable 类的 recordCommandBuffer 函数中执行。 此函数将每个交换链缓冲区的绘图对象的相关命令记录在单独的一个命令缓冲区对象(cmdDraw)中。 该过程包括以下步骤。
使用 vkCmdBeginRenderPass()API 记录 Render Pass 实例;该调用接受 VkRenderPassBeginInfo 类的对象(renderPassBegin)作为输入参数,其中包含 Render Pass 和 framebuffer 对象,用于指示与此 Render Pass 实例关联的附件、子通道以及图像视图。 有关渲染通道和帧缓冲区对象的更多信息,请参阅第 7 章,“缓冲区资源,渲染通道,帧缓冲区和使用 SPIR-V 的着色器”。
renderPassBegin 承载的其他信息就是帧缓冲区上绘图区域的范围。 此外,颜色,深度或模板图像的清除值也在里面设置。 此信息将会使用指定的清除值清除关联的缓冲区附件。 例如,使用颜色缓冲区附件关联的清除值清除后就像背景色一样。
注意
传递给 vkCmdBeginRenderPass 的最后一个参数是 VK_SUBPASS_CONTENTS_INLINE; 它表明只有主命令缓冲区应该记录子通道内容,并且在这个子通道内,不会执行辅助命令。
使用 vkCmdBindPipeline(),并将命令缓冲区与我们在第 8 章“管线和管线状态管理”中创建的图形管线对象绑定。
需要动态设置视口和裁剪区域,请使用 initViewports()和 initScissors(),并使用所需的矩形区域大小设置 vkCmdSetViewport()和 vkCmdSetScissor()API。
注意
在运行时使用 vkCmdSetViewport()和 vkCmdSetScissor()API 指定视口和裁剪尺寸时,必须使用 VK_DYNAMIC_STATE_VIEWPORT 启用动态状态(VkDynamicState)。
使用非索引化的绘图 API vkCmdDraw()指定绘图命令。 第一个参数指定将要在其中记录绘图命令的命令缓冲区对象(VkCommandBuffer)。 第二个参数 3 指定了打算绘制的几何图形的顶点数量。 第三个参数 1 指定需要一次绘制一个实例。 第四个参数指定要绘制的第一个顶点索引(0); 最后一个参数 0 指定要基于实例进行绘图的第一个索引,负责控制数据从实例化数组步进的速率。
使用 vkCmdEndRenderPass()完成 Render Pass 实例的记录操作并将当前命令缓冲区对象传递给它。
记录命令缓冲区的实现如下所示:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw)
{
VulkanDevice* deviceObj = rendererObj->getDevice(); VkClearValue clearValues[2];
clearValues[0].color.float32[0] = 0.0f; clearValues[0].color.float32[1] = 0.0f; clearValues[0].color.float32[2] = 0.0f; clearValues[0].color.float32[3] = 0.0f; clearValues[1].depthStencil.depth = 1.0f; clearValues[1].depthStencil.stencil = 0;
VkRenderPassBeginInfo renderPassBegin;
renderPassBegin.sType = VK_STRUCTURE_TYPE_ RENDER_PASS-_BEGIN_INFO;
renderPassBegin.pNext = NULL;
renderPassBegin.renderPass = rendererObj->renderPass;
renderPassBegin.framebuffer = rendererObj-> framebuffers[currentImage];
renderPassBegin.renderArea.offset.x = 0;
renderPassBegin.renderArea.offset.y = 0; renderPassBegin.renderArea.extent.width = rendererObj->width; renderPassBegin.renderArea.extent.height = rendererObj->height; renderPassBegin.clearValueCount = 2;
renderPassBegin.pClearValues = clearValues;
// Start recording the render pass instance vkCmdBeginRenderPass(*cmdDraw, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE);
// Bound the command buffer with the graphics pipeline
vkCmdBindPipeline(*cmdDraw, VK_PIPELINE_BIND_POINT_GRAPHICS,
*pipeline);
// Bound the command buffer with the graphics pipeline const VkDeviceSize offsets[1] = { 0 }; vkCmdBindVertexBuffers(*cmdDraw, 0, 1, &VertexBuffer.buf, offsets);
// Define the dynamic viewport here
initViewports(cmdDraw);
// Define the scissoring
initScissors(cmdDraw);
// Issue the draw command with 3 vertex, 1 instance starting
// from first vertex
vkCmdDraw(*cmdDraw, 3, 1, 0, 0);
// End of render pass instance recording
vkCmdEndRenderPass(*cmdDraw);
}
渲染绘图对象 drawing object
一旦 Render Pass 实例准备好并成功记录在绘图对象的命令缓冲区中,我们就可以每次重用它来绘制对象了。
绘制一个对象包括三个步骤:首先,我们需要获取下一个可用交换链图像的索引,图元将会在其上(交换链图像)进行绘制或者光栅化。 其次,我们需要将命令缓冲区提交给图形队列,以在 GPU 上执行记录的命令;GPU 会执行这些命令并使用这些绘图命令绘制可用的交换链绘图图像。 最后,绘制图像会被移交给展示引擎,它会把输出渲染到附加的显示窗口上。 以下小节会详细介绍这三个步骤。
获取交换链图像 swapchain image
在执行 Render Pass 实例记录的命令之前,我们需要获取一个在其上执行绘图操作的交换链图像。 为此,我们需要使用 WSI 扩展 vkAcquireNextImageKHR()从系统中查询可用的交换链图像的索引;这会返回应用程序要渲染的交换链图像的索引。 这个扩展是以 PFN_vkAcquireNextImageKHR 类型的函数指针的形式提供的。
在 API 调用时,会获取可展示的图像 ----- 会使用当前的命令缓冲区在其上进行绘制操作,并通知应用程序可展示的目标图像已经发生了改变。
注意
调用这个 API 时,有多种因素会影响可展示图像的可用性。 这其中包括展示引擎的实现,如何使用 VkPresentModeKHR,交换链中的图像总数,应用程序在任意给定的时刻拥有的图像数量,当然还有应用程序的性能。
VkResult vkAcquireNextImageKHR( VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout,
VkSemaphore semaphore,
VkFence fence,
uint32_t* pImageIndex);
其中每个参数的说明如下:
参数 | 描述
—|---
device | 这表示逻辑的 VkDevice,它与交换链对象进行了关联。
swapchain | 这表示要从中获取绘图图像的交换链对象(VkSwapchainKHR)。
timeout | 此参数指示 API 是阻塞的还是非阻塞的。 当指定超时(以纳秒为单位)时,它指示函数等待或阻塞多长时间(如果没有图像可用)。 如果 timeout 为 0,则此 API 不会阻塞并返回成功或失败错误。 此字段保证 vkAcquireNextImageKHR()API 在请求失败时不会阻塞系统并在有限的时间后返回所有权。
semaphore | 这是 VkSemaphore 对象,必须是无信号的,并且不应该有任何未完成的信号或等待的挂起操作。 当展示引擎释放图像的所有权并且设备可以修改其内容时,它将会是有信号的。 如果不使用这个 API,则可以设置为 VK_NULL_HANDLE。
fence | 这是一个 VkFence 对象,并且必须是无信号的,并且没有任何待处理的、未完成的信号操作。 当展示引擎释放图像的所有权时,它将变成有信号的。 如果不使用这个 API,则可以等于 VK_NULL_HANDLE。 这可以用来测量与显示速率相匹配的帧的生成工作。
pImageIndex | 这会检索下一个可呈现图像的索引。 此索引属于由 vkGetSwapchainImagesKHR()API 返回的图像句柄数组中的索引。 如果 API 不返回 VK_SUCCESS 实例,则意味着指向的索引不会被修改。
注意
vkAcquireNextImageKHR()不能同时指定信号量 semaphore 和栏栅 fence VK_NULL_HANDLE; 其中之一必须是一个有效的输入。
下表指定了 vkAcquireNextImageKHR()API 的返回值,该值取决于超时 timeout 参数:
返回值 | 描述
—|---
VK_SUCCESS | 这意味着它已经成功获得了可呈现的图像。 VK_ERROR_SURFACE_LOST_KHR | 这种错误在表面不再可用时会出现。 VK_NOT_READY | 这表示没有图像可用,并且超时时间为 0。
VK_TIMEOUT | 当超时值是非零平均值(> 0 和<UINT64_MAX)时,表示在允许的持续时间内没有可显示的图像可用。
VK_SUBOPTIMAL_KHR | 在这种情况下,返回的可呈现图像不再与交换链表面属性相匹配,但仍可以使用。
VK_ERROR_OUT_OF_DATE_KHR | 在这里,返回的可呈现图像不再与交换链兼容,因此不能再与交换链一起使用,用于显示。 在这种情况下,应用程序必须查询兼容的表面属性,并使用所需的表面属性重新创建交换链,以便继续使用展示功能的服务。
执行绘图命令缓冲区 drawing command buffer object
执行命令缓冲区对象是通过使用 CommandBufferMgr :: submitCommandBuffer()函数将其提交到图形队列中实现的,该函数在内部调用了 vkQueueSubmit()API 提交命令缓冲区。
注意
有关 submitCommandBuffer()API 及其用法的更多信息,请参阅第 5 章,“Vulkan 中的命令缓冲区以及内存管理”。 在本章中,您可以参考“实现命令缓冲区的包装类”下的“提交命令到队列”小节。
使用展示引擎 presentation engine 显示输出
当绘图对象的命令缓冲区被执行时,就会使用记录的命令绘制目标展示图像。 然后使用 vkQueuePresentKHR()API 将该图像排入展示引擎,该 API 会将输出展示图像渲染在显示输出上。
VkResult vkQueuePresentKHR( VkQueue queue,
const VkPresentInfoKHR* pPresentInfo);
vkQueuePresentKHR()API 使用的参数如下所示:
数 | 描述
—|---
queue | 这是一个 VkQueue 对象,能够展示并具有图形队列的功能。 此外,此队列属于和图像交换链相同的设备。
pPresentInfo | 这是一个指向 VkPresentInfoKHR 结构的指针,指定展示元数据。
以下是 VkPresentInfoKHR()的语法和描述:
typedef struct VkPresentInfoKHR {
VkStructureType sType;
const void* pNext;
uint32_t waitSemaphoreCount; const VkSemaphore* pWaitSemaphores;
uint32_t swapchainCount; const VkSwapchainKHR* pSwapchains;
const uint32_t* pImageIndices;
VkResult* pResults;
} VkPresentInfoKHR;
其中的字段说明如下:
字段 | 描述
—|---
sType | 这指定了这个结构的类型;这必须是 VK_STRUCTURE_TYPE_PRESENT_INFO_KHR。
pNext | 这可能是一个指向扩展特定结构的有效指针,也可能是 NULL。 waitSemaphoreCount | 该字段指示显示引擎在显示图像之前应等待的信号量计数。
pWaitSemaphores | 这指定了在发出显示请求之前要等待的信号量。 这是一个大小等于 waitSemaphoreCount 的非空 VkSemaphore 对象数组。
swapchainCount | 可以有多个用于展示的交换链;该字段表示要使用此 API 命令,被呈现的交换链数量。
pSwapchains | 这表示大小等于 swapchainCount 项的 VkSwapchainKHR 对象数组。
pImageIndices | 这是每个交换链的可呈现图像的索引数组,其中总计条目由 swapchainCount 指定。 索引数组中的每个条目都指示在展示中要使用的可呈现图像。
pResults | 该字段(如果非 NULL)返回每个可呈现图像的状态(VkResult 类型); 条目数等于 swapchainCount。 如果应用程序不需要每个交换链的返回结果,则可以将 pResults 设置为 NULL。
注意
vkQueuePresentKHR 能够呈现来自对应交换链的多个可呈现图像。 它将图像的所有权(由 pImageIndices 指示;请参考以下 VkPresentInfoKHR)释放给展示引擎。 在应用程序使用 vkAcquireNextImageKHR()API 重新获得对它们的控制权之前(并且必须等到返回的信号量获得信号通知,即有信号,或者 fence 完成以后),这些可呈现的图像还不能被再次使用。
发送到队列的所有展示图像总是按顺序进行处理;将展示图像的所有权转移给展示引擎会发生在队列中的提交操作过程中。 这些可呈现的图像仅在被提交的信号量获得信号(有信号)时才执行,表示先前的渲染操作都已经完成。 展示的时间是非常特定于具体实现的, 很可能会受到呈现引擎以及正在使用的本地平台的语义的影响。
下表指定了 vkQueuePresentKHR()API 的返回值:
参数 | 描述
—|---
VK_SUCCESS | 这意味着它已经成功获得了可展示的图像。
VK_ERROR_SURFACE_LOST_KHR | 这会在表面不再可用时出现这个返回值。 VK_SUBOPTIMAL_KHR | 在这种情况下,返回的可呈现图像不再与交换链表面属性匹配,但仍可成功用于绘图目的。
VK_ERROR_OUT_OF_DATE_KHR | 这里,返回的可呈现图像不再与交换链兼容,因此不能再与交换链一起使用,进行展示操作。 在这种情况下,应用程序必须查询兼容的表面属性并使用所需的表面属性重新创建交换链,以便继续使用展示服务。
当可展示的图像被指定给展示引擎时,展示操作不会更改该图像的内容。 如果使用 vkAcquireNextImageKHR()再次重新获取该图像,并且将转换从 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 布局中移除,则其内容与转换前的内容保持一致,不会发生变化。 相比之下,如果 Vulkan 以外的某些其他机制修改了平台窗口,则交换链中所有可呈现图像的内容将变成未定义的,具有不确定性。
实现绘图对象渲染 drawing object rendering
让我们来了解一下实现的渲染代码。 首先,使用 vkAcquireNextImageKHR()API 扩展获取可展示的图像索引;该索引是在 currentColorImage 变量中返回的。 交换链扩展存储为 VulkanRender 类中的一个函数指针(fpAcquireNextImageKHR)。
注意
有关查询交换链扩展的更多信息,请参阅第 6 章“分配映像资源并使用 WSI 构建交换链”中的“查询交换链扩展”小节。
您必须至少提供一个同步对象(信号量或栏栅); 否则,您会不知道何时才可以使用该图像。 例如,当您尝试获取图片时,展示引擎仍然会读取您的图片信息。 vkAcquireNextImageKHR()一旦识别出必须提供给你下一个图像的时候,就允许返回这个 API,而不是该图像实际可用时才返回。 出于这个原因,同步在这一步是非常重要的。 Vulkan 提供了两种同步交换链图像的方法,即使用信号量和栏栅。 当使用信号量和栏栅时,它们能够确保在获取图像时,这个图像没有以前的待处理操作(例如展示引擎还在读取这个图像)。
在这个例子中,我们使用一个信号量对象(presentCompleteSemaphore)进行同步;该对象被传递到 vkAcquireNextImageKHR(),以便与图像相关联,并且在能够渲染图像时该信号量就会获得信号通知,处于有信号的状态。
使用检索到的图像索引(currentColorImage),并从 vector vecCmdDraw 中获取相应的命令缓冲区。 创建 VkSubmitInfo 控制结构并指定创建信号量对象(presentCompleteSemaphore)以提交命令缓冲区。 提交后,命令只会在信号量获得信号(有信号状态)时才会开始执行;换句话说,图像已准备好用绘图命令进行绘制了。
作为最后一种方法,使用 fpQueuePresentKHR API(vkQueuePresentKHR)把绘制的图像在展示引擎中排队,用于显示目的,将所有权转移给展示引擎。 确保显示引擎使用图像时,自上次命令缓冲区提交以来,该图像未被正在绘制或有任何挂起的操作,这一点非常重要。 这可以使用名为 drawingCompleteSemaphore 的另一个信号量对象简单地进行检查;将命令缓冲区提交到队列之前,这个对象会被传入 VkSubmitInfo 类 pSignalSemaphores 成员中。 在成功处理好了命令缓冲区时,这个信号量也就有信号了(有信号状态),消除了与展示引擎所有权重叠的机会,消除了冲突。 一旦显示了可展示的图像,展示引擎就会放弃所有权。 vkAcquireNextImageKHR()可以再次查询相同的图像并获得所有权。
以下是演示 Vulkan 中对象渲染的实现代码:
void VulkanDrawable::render()
{
VulkanDevice* deviceObj = rendererObj->getDevice(); VulkanSwapChain* swapChainObj= rendererObj->getSwapChain(); uint32_t¤tColorImage = swapChainObj->
scPublicVars.currentColorBuffer; VkSwapchainKHR& swapChain = swapChainObj->
scPublicVars.swapChain;
VkFence nullFence = VK_NULL_HANDLE;
// Get the index of the next available swapchain image: VkResult result = swapChainObj->fpAcquireNextImageKHR( deviceObj->device, swapChain,UINT64_MAX, presentCompleteSemaphore, VK_NULL_HANDLE, ¤tColorImage);
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
// Prepare the submit into control structure
VkSubmitInfo submitInfo = {};
submitInfo.sType= VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext= NULL; submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores= & presentCompleteSemaphore; submitInfo.pWaitDstStageMask= &submitPipelineStages; submitInfo.commandBufferCount= (uint32_t)sizeof(&vecCmdDraw [currentColorImage]) / sizeof(VkCommandBuffer); submitInfo.pCommandBuffers = &vecCmdDraw[currentColorImage]; submitInfo.signalSemaphoreCount= 1;
submitInfo.pSignalSemaphores = & drawingCompleteSemaphore;
// Queue the command buffer for execution CommandBufferMgr::submitCommandBuffer(deviceObj->queue, &cmdDraw[currentColorImage],&submitInfo);
// Present the image in the window
VkPresentInfoKHR present;
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present.pNext = NULL;
present.swapchainCount= 1; present.pSwapchains= &swapChain; present.pImageIndices= ¤tColorImage;
present.pWaitSemaphores= & drawingCompleteSemaphore; present.waitSemaphoreCount= 1;
present.pResults= NULL;
// Queue the image for presentation, result = swapChainObj->fpQueuePresentKHR (deviceObj->queue, & present); assert(result == VK_SUCCESS);
}
信号量对象是在 drawable 类的构造函数中创建的,并在整个应用程序中重用,如下所示。
VulkanDrawable::VulkanDrawable(VulkanRenderer* parent) { memset(&VertexBuffer, 0, sizeof(VertexBuffer)); rendererObj = parent;
// Prepare the semaphore create info data structure VkSemaphoreCreateInfo presentCompleteSemaphoreCreateInfo;
presentCompleteSemaphoreCreateInfo.sType =
VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
presentCompleteSemaphoreCreateInfo.pNext = NULL; presentCompleteSemaphoreCreateInfo.flags = 0;
VkSemaphoreCreateInfo drawingCompleteSemaphoreCreateInfo; drawingCompleteSemaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
drawingCompleteSemaphoreCreateInfo.pNext = NULL; drawingCompleteSemaphoreCreateInfo.flags = 0;
VulkanDevice* deviceObj = VulkanApplication::GetInstance()-> deviceObj;
vkCreateSemaphore(deviceObj->device, & presentCompleteSemaphoreCreateInfo, NULL, & presentCompleteSemaphore);
vkCreateSemaphore(deviceObj->device, & drawingCompleteSemaphoreCreateInfo, NULL,
& drawingCompleteSemaphore);
}
在析构过程中这些信号量对象可以使用用户定义的 destroySynchronizationObjects()函数进行销毁:
void VulkanDrawable::destroySynchronizationObjects()
{
VulkanApplication* appObj = VulkanApplication::GetInstance(); VulkanDevice* deviceObj = appObj->deviceObj;
vkDestroySemaphore(deviceObj->device, presentCompleteSemaphore, NULL); vkDestroySemaphore(deviceObj->device, drawingCompleteSemaphore, NULL);
}
以下是该程序的输出:
渲染索引化的几何图形 ndexed geometry
在本节中,您将学习使用 vkCmdDrawIndexed()绘图命令, 该命令用于绘制索引化的几何图形。vkCmdDrawIndexed()API 是索引缓冲区的绘图命令。 在索引缓冲区中,每个顶点都使用索引号表示。 这种代表网格 mesh 数据的方式需要较少的内存和存储空间来表示网格 mesh 拥有共享顶点时(比如闭合的形状)互相连接的 mesh 。
例如,使用两个三角形渲染的正方形几何图形共享两个公共顶点,如以下示例中所示;如你所见,第一个和第三个顶点重复:
static const VertexWithColor squareData[] =
{
{ -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0 },
{ 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0 },
{ 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0 },
{ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0 },
};
uint16_t squareIndices[] = { 0,3,1, 3,2,1 }; // 6 indices
在本节中,我们将使用这个几何图形的数据和索引来演示如何使用 vkCmdDrawIndexed()API。 以下是它的语法:
void vkCmdDrawIndexed(
VkCommandBuffer commandBuffer, uint32_t indexCount,
uint32_t instanceCount,
uint32_t firstIndex,
int32_t vertexOffset,
uint32_t firstInstance);
我们来看看这个 API 中使用的不同参数及其各自的描述:
参数 | 描述
—|---
commandBuffer | 该参数指定执行记录的命令缓冲区对象(VkCommandBuffer)。 indexCount | 这是要绘制的缓冲区资源的索引列表中索引元素的总数。
instanceCount | 该参数表示使用此命令要绘制的实例总数。
firstIndex | 该参数指定读取索引中的第一个索引(索引序列 indices 中的)。
vertexOffset | 此参数指定要被添加到顶点索引以产生结果索引的偏移值。 这个计算出来的索引会被用来从顶点缓冲区读取顶点信息。
firstInstance | 该参数指定要绘制实例中的第一个实例 ID。
以下是渲染索引化的绘图的步骤:
- 使用 squareData 和 squareIndices,创建一个缓冲区资源(VkBuffer)。 将 VkBuffer 的句柄存储在 VertexBuffer :: buf 和 VertexIndex :: idx 中。 有关更多信息,请参阅本章提供的“索引化的绘图”示例。
- 使用 vkCmdBindVertexBuffers()绑定顶点缓冲区并将 VertexBuffer.buf 传递给它。
- 类似地,使用 vkCmdBindIndexBuffer()API 命令绑定索引缓冲区(VertexIndex.idx)。
- 使用 vkCmdDrawIndexed()绘制对象。
vkCmdDrawIndexed()与 vkCmdBindIndexBuffer()API 结合使用。 与绑定顶点缓冲区的 vkCmdBindVertexBuffers()类似,该命令会绑定索引缓冲区。 以下是此 API 的语法;有关 vkCmdBindVertexBuffers()API 的更多信息,请参阅“指定绘图对象几何信息”部分。
void vkCmdBindIndexBuffer(
VkCommandBuffer commandBuffer,
VkBuffer buffer,
VkDeviceSize offset,
VkIndexType indexType);
我们来看看这个 API 中使用的不同参数及其各自的描述:
参数 | 描述
—|---
commandBuffer | 这指定要记录此命令 -vkCmdBindIndexBuffer()的命令缓冲区对象。 buffer | 这表示要绑定到此 API 的索引缓冲区(VkBuffer)的句柄。
offset | 这是索引缓冲区中以字节为单位指定的起始偏移量,会用于索引缓冲区的地址计算。 indexType | 该参数指示索引是 16 位还是 32 位宽。 这必须是 VkIndexType 类型之一:
Typedef enum VkIndexType {VK_INDEX_TYPE_UINT16 = 0,
VK_INDEX_TYPE_UINT32 = 1,
} VkIndexType;
与 createVertexBuffer()函数类似,我们创建了一个名为 createIndexBuffer()的新函数;它创建索引缓冲区并将索引缓冲区句柄存储在 VertexIndex.idx 中。 有关此函数的详细实现,请参阅随附的源代码。 createIndexBuffer()的实现与 createVertexBuffer()非常相似;有关该函数实现的详细信息,请参阅第 7 章“缓冲区资源,渲染通道,帧缓冲区以及使用 SPIR-V 的着色器”中的“实现缓冲区资源 - 为几何图形创建缓冲区资源 ”。
以下代码演示了索引化的几何对象的渲染过程:
// Local data structure in VulkanDrawable class
// for storing vertex buffer and index buffer metadata
struct {
VkBuffer buf; VkDeviceMemory mem;
VkDescriptorBufferInfo bufferInfo;
} VertexBuffer;
struct {
VkBuffer idx; VkDeviceMemory mem;
VkDescriptorBufferInfo bufferInfo;
} VertexIndex;
// Create the VkBuffer and store the handle in
// VertexBuffer.buf and VertexIndex.idx
. . . .
// Bind the vertex buffer
const VkDeviceSize offsets[1] = { 0 }; vkCmdBindVertexBuffers(*cmdDraw, 0, 1,&VertexBuffer.buf,
offsets);
// Bind the Index buffer
vkCmdBindIndexBuffer(*cmdDraw, VertexIndex.idx,
0, VK_INDEX_TYPE_UINT16);
// Draw the object
vkCmdDrawIndexed(cmdDraw, 6, 1, 0, 0, 0);
上述几何数据的输出效果显示如下: 有关详细代码,请参阅本章中的“索引化的绘图”示例:
vkCmdDrawIndirect 和 vkCmdDrawIndexedIndirect 绘图命令与 vkCmdDraw 和 vkCmdDrawIndexed 非常相似,只是这里的参数是从缓冲区内存中读取的。 有关这些 API 的更多信息,请参阅官方的 Vulkan 规范。
理解 Vulkan 中的同步原语 synchronization primitives
同步是将顺序和约束引入异步系统的关键。 它不仅提高了资源利用率,而且还通过减少 CPU 和 GPU 空闲时间从并行性中受益。
Vulkan 为并发执行提供以下四种类型的同步原语:
- 栅栏:Fences,提供主机和设备之间的同步
- 信号量:Semaphores,在队列之间以及队列内部进行同步
- 事件:Events,对队列提交操作之间提供同步
- 障碍:Barriers,在一个命令缓冲区内的命令之间进行同步
在本节中,我们将学习同步原语并理解他们的 API 规范。 我们在本章中实现的绘图对象示例使用信号量来同步交换链图像的写入操作。 在下一章中,我们将学习绘制纹理并实现栏栅,以便同步主机和设备。
Fences 栏栅
当主机在队列中提交命令时,会对主机进行调度,以便进行设备的处理。 有时主机可能需要知道 GPU 上命令执行的状态,以便控制下一个批处理的执行,以此来确保它不会与前一批命令重叠,这可能会产生未定义的结果或导致资源访问非法的情况 。
栏栅提供主机和 GPU 之间的同步;使用栏栅,应用程序指示主机等待某个提交的操作完成。 这样,就可以防止 GPU 将更多操作堆积到命令队列中:
创建栏栅 fence 对象:可以使用 vkCreateFence()API 创建栏栅对象。
VkResult vkCreateFence(
VkDevice device,
const VkFenceCreateInfo pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkFence* pFence);
我们来看看这个 API 中使用的不同参数及其各自的描述:
参数 | 描述 -
–|—
device | 这是逻辑设备对象,用于创建 fence 对象。
pCreateInfo | 这是一个指向 VkFenceCreateInfo 控制结构数组的指针。
pAllocator | 这个参数控制了主机内存的分配。 有关更多信息,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”。
pFence | 这是创建的栏栅对象的句柄。
vkCreateFence()API 采用包含了元数据的 VkFenceCreateInfo 结构作为参数,该元数据用于控制 fence 对象的创建。 以下是这个控制结构的语法:
typedef struct VkFenceCreateInfo {
VkStructureType sType;
const void* pNext;
VkFenceCreateFlags flags;
} VkFenceCreateInfo;
它有三个字段:第一个字段 sType 表示结构的类型信息,必须是 VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; 第二个字段 pNext 未被使用且必须为 NULL; 最后一个参数是 VkFenceCreateFlagBits(请参阅下面的代码片段),表示创建的栏栅对象是处于有信号还是无信号状态。 信号状态使用 VK_FENCE_CREATE_SIGNALED_BIT 指定。
typedef enum VkFenceCreateFlagBits {
VK_FENCE_CREATE_SIGNALED_BIT = 0x00000001,
} VkFenceCreateFlagBits;
等待栅栏 fence 对象:一旦创建了一个有效的 fence 对象,主机就可以将其注入到命令中并使用 vkWaitForFences()API 等待 fence 对象,直到设备处理不对其进行处理。 设备在处理完相关命令后立即给 fence 对象发送信号,这样就会允许主机解锁等待状态。 以下是 API 的语法:
VkResult vkWaitForFences(
VkDevice device,
uint32_t fenceCount,
const VkFence* pFences,
VkBool32 waitAll,
uint64_t timeout);
vkWaitForFences API 采用以下参数:
参数 | 描述
—|---
device | 这是要用于销毁栏栅对象的逻辑设备对象(VkDevice)。
fenceCount | 这是需要销毁的栏栅对象的数量。
PFences | 这是一个需要销毁的栏栅对象句柄数组。 数组大小必须等于 fenceCount。 waitAll | 阻塞状态可以通过使用此布尔标志解除阻塞。 当这个标志值是:
VK_TRUE:表示所有的 pFences 都必须是有信号的,以便成功解除等待。
VK_FALSE:pFences 数组中的至少一个 fence 对象有信号,才能成功解除等待状态。 timeOut | 这是指定的超时期限(以纳秒为单位),如果栏栅对象从未获得信号,则超时期限会用于解锁等待状态。 该参数保证系统不会陷入无限的阻塞状态,从而导致应用程序停止。
销毁栏栅 fence 对象:栏栅一旦被使用且不再需要的时候,就可以使用 vkDestroyFence()API 销毁栏栅对象;这个 API 有三个参数 - 第一个参数 device 是用来销毁 fence 对象的逻辑设备,它由第二个名为 fence 的参数表示;最后一个参数(pAllocator)管理主机内存的释放:
void vkDestroyFence(
VkDevice device,
VkFence fence,
const VkAllocationCallbacks* pAllocator);
重置栅栏 fence 对象:应用程序还可以保留创建的 fence 对象,并通过使用 vkResetFences()重置它们来对其进行重用。 该 API 有三个输入参数:第一个参数表示要用来重置给定栏栅对象的逻辑设备。 被称为 fenceCount 的第二个参数指示需要重置的 fence 对象的数量。 最后一个参数 pFences 是要由该 API 进行重置的栏栅对象数组的指针。 以下是此 API 的语法:
VkResult vkResetFences(
VkDevice device,
uint32_t fenceCount,
const VkFence* pFences);
让我们转到下一个同步原语:信号量 semaphores。
Semaphores 信号量
信号量提供了在队列级别实现同步的灵活性;它们用于同步一个或多个队列。 信号量有两种状态:有信号和无信号。 有信号的信号量在队列提交命令 vkQueueSubmit()中指定;它会阻塞批处理的其余部分,直到设备获得信号量的信号。 创建的信号量在多个队列中可见。 如果两个或多个队列提交命令在同一信号量上等待,则只有一个会收到有信号状态;其他队列可能会继续等待,以便确保原子性。
创建信号量 semaphore 对象:使用 vkCreateSemaphore()API 创建信号量;以下是此 API 的语法:
VkResult vkCreateSemaphore(
VkDevice device,
const VkSemaphoreCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSemaphore* pSemaphore);
vkCreateSemaphore API 采用以下参数:
参数 | 描述
—|---
device | 这是逻辑设备对象,用于创建信号量对象。
pCreateInfo | 这是一个指向 VkSemaphoreCreateInfo 控制结构的数组的指针。
pAllocator | 这个参数控制主机内存的分配。 有关更多信息,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pSemaphore | 这是创建的信号量对象的句柄。
VkSemaphoreCreateInfo 类型结构有三个参数: 第一个参数 sType 表示该控制结构的类型信息;第二个参数是 pNext,它可能是一个指向扩展特定结构的有效指针,或者可能为 NULL。 最后一个参数是一个标志值(flags),目前未被使用,并为将来的目的而保留。 以下是这个结构的语法:
typedef struct VkSemaphoreCreateInfo {
VkStructureType sType;
const void* pNext;
VkSemaphoreCreateFlags flags;
} VkSemaphoreCreateInfo;
销毁信号量 semaphore:使用以下语法中声明的 vkDestroySemaphore()销毁创建的信号量。 这个 API 有三个参数: 第一个参数 device 是要销毁信号量对象的逻辑设备,第二个参数(信号量 semaphore)指定的是要销毁的信号量对象。 最后一个参数(pAllocator)管理主机内存的释放:
void vkDestroySemaphore(
VkDevice device,
VkSemaphore semaphore,
const VkAllocationCallbacks* pAllocator);
注意
在本章中,我们使用信号量来确保给定的交换链图像只在展示引擎没有读取它时才可以使用,换句话说,当展示引擎读完交换链图像并准备好渲染它时才能够使用指定的交换链图像。 为此,我们创建了一个信号量对象并将其传递给 vkAcquireNextImageKHR(); 此 API 在图像准备好渲染时会向信号量发送信号。 这个有信号的信号量接下来通过 VkSubmitInfo 控制结构传递给 vkQueueSubmit(); 这确保了绘图命令只能在未使用时才会被绘制到展示图像上。 vkQueueSubmit()会取消信号量的信号,对下一个要执行的命令(vkQueuePresentKHR)解除阻塞;该命令会将图像渲染到输出显示。
事件 Events
事件控制着细粒度的同步,并且可以以有信号和无信号两种状态存在。 它允许在单个命令缓冲区或提交给队列的多个命令缓冲区序列内进行同步操作。 主机和设备都可以发信号或重置事件。 同样,两者都可以等待事件对象;然而,设备只允许在管线内的某个特定管线阶段等待。 随着我们不断介绍 API 规范,您将会学到更多内容:
创建事件 event 对象:可以使用 vkCreateEvent()API 创建事件。 该 API 接受三个参数;语法如下:
VkResult vkCreateEvent(
VkDevice device,
const VkEventCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkEvent* pEvent);
vkCreateEvent API 接受四个参数,如下表所述:
参数 | 描述
—|---
device | 这是逻辑设备对象,用于创建事件对象。
pCreateInfo | 这是一个指向 VkEventCreateInfo 控制结构数组的指针。
pAllocator | 这个参数控制主机内存的分配。 有关更多信息,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pSemaphore | 这是创建的事件对象的句柄。
VkEventCreateInfo 结构有三个参数:第一个参数(sType)描述创建的这个信息数据结构的类型信息;它必须是 VK_STRUCTURE_TYPE_EVENT_CREATE_INFO。 第二个参数是 pNext; 这可能是一个指向扩展特定结构的有效指针,也可能是 NULL。 最后一个参数是标志值(flags),该标志值当前未被使用并保留用于将来目的。 以下是这个结构的语法:
typedef struct VkEventCreateInfo {
VkStructureType sType;
const void* pNext;
VkEventCreateFlags flags;
} VkEventCreateInfo;
销毁事件 event 对象:当事件不再使用时,使用 vkDestroyEvent()销毁创建的事件。 此 API 有三个参数 :第一个参数 device 是要销毁事件对象的逻辑设备,第二个参数(信号量 semaphore)指定了要销毁的事件对象。 最后一个参数(pAllocator)管理主机内存的释放。
void vkDestroyEvent(
VkDevice device,
VkEvent event,
const VkAllocationCallbacks* pAllocator);
查询事件 event 状态:可以查询事件,以检查它是处于有信号状态还是无信号状态。 这是通过使用 vkGetEventStatus()API 完成的;该 API 的第一个参数(device)是拥有事件对象的逻辑设备(VkDevice) ,第二个参数(event)是查询其状态的事件句柄。 以下是此 API 的语法:
VkResult vkGetEventStatus(
VkDevice device,
VkEvent event);
该 API 会返回 VK_EVENT_SET,表示事件是有信号的;对于无信号的事件,会返回 VK_EVENT_RESET。
设置和重置事件 events:可以使用 vkSetEvent()和 vkResetEvent()设置事件。 这两个 API 都接受与上述 vkGetEventStatus()API 相同的输入参数;有关更多信息,请参阅 vkSetEvent 和 vkResetEvent 语法:
vkSetEvent API | vkResetEvent API
—|---
VkResult vkCmdSetEvent( VkDevice device, VkEvent event);| VkResult vkSetEvent( VkDevice device, VkEvent event);
从设备发出有信号和无信号事件 触发事件 重置事件:Signaling and unsignaling an event from a device,可以使用命令缓冲区更新事件,用于对设备进行设置或重置。 vkCmdSetEvent()和 vkCmdResetEvent()API 分别用于设置事件的信号和取消事件的信号;以下是这些 API 的语法:
vkCmdSetEvent API
VkResult vkCmdSetEvent(
VkCommandBuffer
commandBuffer,
VkEvent
event,
VkPipelineStageFlags
stageMask);
vkCmdResetEvent API
VkResult vkCmdResetEvent(
VkCommandBuffer
commandBuffer,
VkEvent
event,
VkPipelineStageFlags
stageMask);
两个 API 都接受以下三个参数:第一个参数(commandBuffer)指定要在其中记录命令的命令缓冲区。 第二个参数(事件)指示事件对象的句柄,该事件对象需要被设置了信号或者取消了信号。 最后一个参数(stageMask)是 VkPipelineStageFlags 管线阶段,指示更新事件状态的时间点。
等待事件对象 event objects:可以使用 vkCmdWaitEvents()API 等待一个或多个事件对象,直到它们是有信号的。 以下是此 API 的语法:
void vkCmdWaitEvents(
VkCommandBuffer commandBuffer,
uint32_t eventCount,
const VkEvent* pEvents,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
uint32_t memoryBarrierCount, const VkMemoryBarrier* pMemoryBarriers,
uint32_t bufferMemoryBarrierCount, const VkBufferMemoryBarrier* pBufferMemoryBarriers, uint32_t imageMemoryBarrierCount, const VkImageMemoryBarrier* pImageMemoryBarriers);
其中每个参数的说明如下:
参数 | 描述
—|---
commandBuffer | 这是捕获或记录命令到其中的命令缓冲区对象。
eventCount | 这是要等待的事件对象的数量。
pEvents | 这是 VkEvent 对象的数组;数组的大小必须等于 eventCount。
srcStageMask | 这是指定管线阶段的位掩码字段,该阶段会设置 pEvents 数组中指定的事件对象的信号。
dstStageMask | 这是指定管线阶段的位掩码字段,在该阶段应执行等待。 memoryBarrierCount | 这是指内存屏障(栅栏 barriers)的数量。
pMemoryBarriers | 这是 VkBufferMemoryBarreir 对象数组,其元素个数等于 memoryBarrierCount。
bufferMemoryBarrierCount | 这是指缓冲区内存屏障(栏栅 barriers)的数量。
pBufferMemoryBarriers | 这指的是元素个数等于 bufferMemoryBarrierCount 的 VkMemoryBarreir 对象数组。
imageMemoryBarrierCount | 这是指图像类型的内存屏障(栅栏 barriers)的数量。
pImageMemoryBarriers | 这指的是元素个数等于 imageMemoryBarrierCount 的 VkImageMemoryBarrier 对象数组。
注意
在第 6 章“分配图像资源以及使用 WSI 构建交换链”中已经讨论并实现了屏障 Barrier 。 有关更多信息,请参阅“使用内存屏障的图像布局转换”部分。
调整显示窗口 display window 的大小
当显示窗口调整大小时,Vulkan 应用程序会获得新的窗口尺寸以重新绘制图形图像。 在 Windows 平台上,可以使用关联的窗口过程的 WM_SIZE 消息来指示尺寸大小的更改,如以下高亮显示的代码所示。 新添到 WndProc()函数所做的更改以粗体突出显示;使用 setSwapChainExtent()函数将新的尺寸大小更新到 VulkanSwapChain 类,该函数稍后会用于重新创建新的交换链图像,使用指示的尺寸:
LRESULT CALLBACK VulkanRenderer::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
VulkanApplication* appObj = VulkanApplication::GetInstance(); switch (uMsg)
{
case WM_CLOSE:
PostQuitMessage(0); break;
case WM_PAINT:
// Many lines skipped please, refer to the source code case WM_SIZE:
if (wParam != SIZE_MINIMIZED) {
appObj->rendererObj->width = lParam & 0xffff; appObj->rendererObj->height = (lParam & 0xffff0000) >> 16;
appObj->rendererObj->getSwapChain()-> setSwapChainExtent(appObj->rendererObj-> width, appObj->rendererObj->height); appObj->resize()
}
break;
default: break;
}
return (DefWindowProc(hWnd, uMsg, wParam, lParam));
}
VulkanApplication 类添加了一个名为 resize()的新函数;此函数会处理调整尺寸的操作。 当发生调整尺寸事件时,Vulkan 应用程序的调整尺寸函数由 VulkanRenderer :: WndProc()来调用。
resize()函数销毁创建的资源并再次重新创建它们。 可以使用 VulkanApplication 中的 isPrepared 标志来检查资源的准备状态;如果此标志为 false,则表示资源尚未准备好,并且无法执行调整大小。
重新创建资源时,旧的交换链图像会被销毁并再次创建以匹配新的窗口大小。
注意
交换链图像必须在未被任何未决命令或展示操作使用时才能重新创建。 这可以通过调用 vkDeviceWaitIdle()API 来确保;这个 API 保证这个设备上没有挂起的操作。 该 API 使主机处于等待状态,直到设备空闲。
当在 resize 函数中等待操作结束时,它可以继续并重新创建交换链图像;但在此之前,我们还需要销毁并重新创建帧缓冲区,命令池,图形管线,渲染通道,深度缓冲区图像,图像视图,顶点缓冲区等。 以下是显示该功能的 resize 函数实现:
void VulkanApplication::resize()
{
// If prepared then only proceed for if (!isPrepared) {
return;
}
isResizing = true;
vkDeviceWaitIdle(deviceObj->device); rendererObj->destroyFramebuffers(); rendererObj->destroyCommandPool(); rendererObj->destroyPipeline();
rendererObj->getPipelineObject()->destroyPipelineCache(); rendererObj->destroyRenderpass();
rendererObj->getSwapChain()->destroySwapChain(); rendererObj->destroyDrawableVertexBuffer(); rendererObj->destroyDepthBuffer();
rendererObj->initialize(); prepare();
isResizing = false;
}
应用程序必须在调整大小的时候,防止进行任何渲染操作;为此,isResizing 标志可用于指示调整尺寸的状态。 重新初始化是通过调用 VulkanRender 的 initialialize()函数来完成的。 绘图命令记录在 Vulkan 应用程序的 prepare 函数中:
void VulkanRenderer::initialize()
{
// We need command buffers, so create a command buffer pool
createCommandPool();
// Let’s create the swap chain color images and depth image
buildSwapChainAndDepthImage();
// Build the vertex buffer
createVertexBuffer();
const bool includeDepth = true;
// Create the render pass now…
createRenderPass(includeDepth);
// Use render pass and create frame buffer
createFrameBuffer(includeDepth);
// Create the vertex and fragment shader
createShaders();
// Manage the pipeline state objects
createPipelineStateManagement();
}
void VulkanApplication::prepare()
{
isPrepared = false; rendererObj->prepare(); isPrepared = true;
}
以下屏幕截图显示了调整窗口尺寸实现的输出效果:
总结
在本章中,我们渲染了我们的第一个 Vulkan 中的 Hello World! 程序。 绘制过程由两个阶段组成 - 准备和渲染(执行)。 准备阶段记录命令缓冲区以及渲染阶段执行这些命令缓冲区中记录的指令。 命令缓冲区会被记录一次并执行多次,除非管线状态对象中有显式的变更。
命令缓冲区的准备阶段包括记录 Render Pass 和图形管线绑定以及绘图参数,例如顶点缓冲区,视口,裁剪器等等。 最后,指定绘图 API 命令;我们使用示例程序演示了基于索引和非索引的绘图 API。
记录的命令缓冲区在渲染阶段执行。 执行过程会获取交换链图像并将命令缓冲区提交到图形队列中。 绘图操作是在获得的交换链图像上完成的。 完成此操作后,图像就会发送到展示引擎以显示在输出显示屏上。 本章还演示了用于绘制索引和非索引几何图形的 API 命令。 在本章末尾,我们介绍了 Vulkan 中的同步原语,并学习了如何在主机和设备以及队列和命令缓冲区之间进行同步。
在下一章中,我们将学习描述符和 push 常量,它允许与 Shader 程序共享资源内容。