Vulkan 围炉夜话3:https://developer.aliyun.com/article/1598111
创建计算流水线 —— vkCreateComputePipelines
计算流水线中包括了一个独立的静态计算着色器阶段,以及流水线布局。计算着色器阶段可以实现大量的并行计算操作。另外,它的流水线布局可以将计算流水线与描述符连接在一起。
VKAPI_ATTR VkResult VKAPI_CALL vkCreateComputePipelines( VkDevice device, VkPipelineCache pipelineCache, uint32_t createInfoCount, const VkComputePipelineCreateInfo* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkPipeline* pPipelines);
- VkComputePipelineCreateInfo
typedef struct VkComputePipelineCreateInfo { VkStructureType sType; const void* pNext; VkPipelineCreateFlags flags; VkPipelineShaderStageCreateInfo stage; VkPipelineLayout layout; VkPipeline basePipelineHandle; int32_t basePipelineIndex; } VkComputePipelineCreateInfo;
流水线状态对象
流水线中的流水线状态对象指的是控制物理设备的硬件设置接口的方法。流水线状态对象有很多种不同的类型,采用预定义的多个阶段的顺序来执行工作。各个阶段的输入数据和资源都可以按照用户自定义的方式进行修改。每个阶段都会处理自己的输入数据并将它传递给下一个阶段。根据应用程序需求的不同,某个流水线状态阶段可以按照用户需要直接略过。
动态状态:设置流水线中用到的动态状态。流水线会通过这个数组来决定哪些状态可以在运行时被修改。
顶点输入状态:设置数据输入的频率和解析方法。
输入装配状态:将顶点数据装配成为不同的几何图元拓扑结构(线、点、各种三角形)。
光栅化状态:有关光栅化的操作,例如多边形填充的模式、面的朝向设置、裁减模式,等等。
颜色融混状态:设置源片元和目标片元之间的融混因子和操作方式。
视口状态:定义视口的裁切方式和维度。
深度/模板状态:定义深度/模板的操作方式。
绘制
绘制过程
在 Vulkan 中实现绘制对象的方法非常简单,其中包括两个部分:准备过程(即构建绘制对象)以及渲染过程。前一个过程负责构建指令缓存,录制绘制指令。后一个过程负责执行指令缓存中的绘制指令,实现物体的渲染。我们详细地介绍一下这两个阶段的内容:
准备可绘制对象:这个过程负责构建指令缓存,以便绘制物体:
指令缓存的创建:对于每一幅交换链的颜色图,我们都要创建一个对应的指令缓存对象。举例来说,如果交换链使用了双缓存的方式,那么它就会包括两幅图像。因此我们需要创建两个指令缓存,以便与图像一一对应。
指令缓存的录制:指令缓存创建完成后,我们将开始录制渲染通道实例的各个子通道对应的指令。请参看下面的步骤:
将创建后的渲染通道对象以及指定大小的帧缓存与渲染区域关联在一起。
设置清除背景颜色和深度图像的初始值。
绑定图形流水线对象。
绑定流水线的资源,包括顶点缓存和描述符集合。
定义视口和裁切区域。
绘制物体。
渲染可绘制对象:现在,我们已经在准备过程中完成了指令缓存的录制,并且会反复不停地使用它来实现可绘制对象的渲染:
获取当前可以执行渲染的交换链图像。
发送指令缓存,实现物体的渲染。
将渲染完成的绘制图像显示到展示引擎端。
录制渲染通道指令
渲染通道实例每次会针对一个子通道进行指令的录制。渲染通道中可能包括一个或者多个子通道。对于每个子通道,都需要调用 API 函数 vkCmdBeginRenderPass() 和 vkCmdEndRenderPass() 来进行指令的录制。这两个函数共同定义了一个录制范围,并针对特定的某个子通道记录各种类型的指令。
vkCmdBeginRenderPass —— 开始录制渲染通道
VKAPI_ATTR void VKAPI_CALL vkCmdBeginRenderPass( VkCommandBuffer commandBuffer, const VkRenderPassBeginInfo* pRenderPassBegin, VkSubpassContents contents);
- VkSubpassContents
typedef enum VkSubpassContents { // 主指令缓存会直接将子通道的内容记录下来,子通道内的次要指令缓存并不会被执行。 VK_SUBPASS_CONTENTS_INLINE = 0, // 次要指令缓存会被激活,并且负责记录子通道的指令内容。 VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS = 1, VK_SUBPASS_CONTENTS_MAX_ENUM = 0x7FFFFFFF } VkSubpassContents;
- VkRenderPassBeginInfo
typedef struct VkRenderPassBeginInfo { VkStructureType sType; const void* pNext; VkRenderPass renderPass; // 渲染通道实例 VkFramebuffer framebuffer; // 帧缓存实例 VkRect2D renderArea; // 渲染区域范围 uint32_t clearValueCount; // 颜色或者深度的清除默认值数量 const VkClearValue* pClearValues; // 清除默认值数组 } VkRenderPassBeginInfo;
- VkRect2D
typedef struct VkRect2D { VkOffset2D offset; VkExtent2D extent; } VkRect2D; typedef struct VkOffset2D { int32_t x; int32_t y; } VkOffset2D; typedef struct VkExtent2D { uint32_t width; uint32_t height; } VkExtent2D;
- VkClearValue
typedef union VkClearValue { VkClearColorValue color; VkClearDepthStencilValue depthStencil; } VkClearValue; typedef union VkClearColorValue { float float32[4]; int32_t int32[4]; uint32_t uint32[4]; } VkClearColorValue; typedef struct VkClearDepthStencilValue { float depth; uint32_t stencil; } VkClearDepthStencilValue;
vkCmdNextSubpass —— 过渡到下一个子通道
VKAPI_ATTR void VKAPI_CALL vkCmdNextSubpass( VkCommandBuffer commandBuffer, VkSubpassContents contents); typedef enum VkSubpassContents { // 主指令缓存会直接将子通道的内容记录下来,子通道内的次要指令缓存并不会被执行。 VK_SUBPASS_CONTENTS_INLINE = 0, // 次要指令缓存会被激活,并且负责记录子通道的指令内容。 VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS = 1, VK_SUBPASS_CONTENTS_MAX_ENUM = 0x7FFFFFFF } VkSubpassContents;
vkCmdEndRenderPass—— 结束录制渲染通道
VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass( VkCommandBuffer commandBuffer);
vkCmdBindPipeline—— 绑定流水线对象
在渲染通道实例中,我们要做的第一件事就是使用函数 vkCmdBindPipeline() 绑定流水线对象。这个函数负责绑定一个特定的流水线(图形或者计算)到当前的指令缓存中
VKAPI_ATTR void VKAPI_CALL vkCmdBindPipeline( VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline);
- VkPipelineBindPoint
typedef enum VkPipelineBindPoint { // 图形流水线 VK_PIPELINE_BIND_POINT_GRAPHICS = 0, // 计算流水线 VK_PIPELINE_BIND_POINT_COMPUTE = 1, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR = 1000165000, VK_PIPELINE_BIND_POINT_SUBPASS_SHADING_HUAWEI = 1000369003, VK_PIPELINE_BIND_POINT_RAY_TRACING_NV = VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, VK_PIPELINE_BIND_POINT_MAX_ENUM = 0x7FFFFFFF } VkPipelineBindPoint;
图形流水线、计算流水线,绑定之后能够执行的指令是完全不同的:
计算流水线:如果流水线绑定为 VK_PIPELINE_BIND_POINT_COMPUTE,那么只有 vkCmdDispatch 和 vkCmdDispatchindirect 指令可以使用。其他所有的指令在当前流水线状态下都不受影响。
图形流水线:如果流水线绑定为 VK_PIPELINE_BIND_POINT_GRAPHICS,那么vkCmdDraw,vkCmdDrawIndexed,vkCmdDrawIndirect 以及 vkCmdDrawIndexedIndirect 指令都可以使用。其他指令在当前流水线状态下都不受影响。
vkCmdBindDescriptorSets —— 绑定描述符集
VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorSets( VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipelineLayout layout, // 设置第一个要绑定的描述符集的索引位置 uint32_t firstSet, // 设置 pDescriptorSets 数组中的元素总数 uint32_t descriptorSetCount, // 描述符集数组 const VkDescriptorSet* pDescriptorSets, // 设置 pDynamicOffsets 数组中动态偏移值元素的总数 uint32_t dynamicOffsetCount, // 动态偏移值数组 const uint32_t* pDynamicOffsets);
vkCmdBindVertexBuffers —— 设置绘制对象的几何体信息
绘制对象的几何体信息需要通过顶点缓存的方式来表示。顶点缓存创建赋值参考 VkBuffer 相关
VKAPI_ATTR void VKAPI_CALL vkCmdBindVertexBuffers( VkCommandBuffer commandBuffer, // 设置顶点输入绑定的索引 uint32_t firstBinding, // 设置顶点输入绑定的数量 uint32_t bindingCount, // 设置 VkBuffer 数组 const VkBuffer* pBuffers, // 设置顶点缓存偏移量的数组 const VkDeviceSize* pOffsets);
vkCmdSetViewport —— 定义动态视口
视口定义了当前绘制表面的区域,可绘制的图元内容将会渲染到这个区域当中。视口的参数控制可以采用静态或者动态的方式来实现:
静态控制:如果我们禁止了动态状态 VK_DYNAMIC_STATE_VIEWPORT,那么我们将无法修改视口参数的内容,只能在创建流水线状态对象(使用 VkPipelineViewportStateCreateInfo 类的成员函数 pViewport 实现)时设置一次。
动态控制:另一方面,如果我们在创建流水线状态对象的时候启用了动态状态 VK_DYNAMIC_STATE_VIEWPORT,那么我们就可以在运行时直接修改视口变换的参数。这些参数可以通过 API 函数 vkCmdSetViewport() 来实现动态的控制。
VKAPI_ATTR void VKAPI_CALL vkCmdSetViewport( VkCommandBuffer commandBuffer, // 视口数组数据的索引值 uint32_t firstViewport, // 视口数组的元素数量 uint32_t viewportCount, // 视口数组数据 const VkViewport* pViewports);
vkCmdSetScissor —— 定义裁切区域
裁切操作定义了一个矩形的区域,帧缓存中的任意片元位置(x,y)如果落在这个矩形区域之外,都会被直接舍弃。
如果流水线状态对象的创建的时候没有启用动态状态 VK_DYNAMIC_STATE_SCISSOR,那么我们只能通过 VkPipelineViewportStateCreateInfo 类的成员 pScissors 来实现裁切矩形的控制。反之,如果流水线状态对象创建的时候启用了动态状态 VK_DYNAMIC_STATE_SCISSOR,那么我们可以通过 API 函数 vkCmdSetScissor() 来实现动态的裁切矩阵设置操作。
和视口参数类似,裁切参数也可以实现静态或者动态的控制:
静态控制:如果我们不准备改变裁切的参数,也就是希望固定裁切尺寸信息。那么我们可以禁用 VK_DYNAMIC_STATE_SCISSOR 来通知底层的流水线。此时流水线默认裁切参数是静态设置的,这样有利于我们判断以及避免任何需要动态控制裁切参数的操作。在进行裁切的静态参数配置时,我们需要使用流水线状态的控制类 VkPipelineViewportStateCreateInfo 的成员 pScissors 。
动态控制:另一方面,如果我们启用了动态状态 VK_DYNAMIC_STATE_SCISSOR,就可以通过一个特定的 API 函数 vkCmdSetScissor() 动态地修改裁切参数。
VKAPI_ATTR void VKAPI_CALL vkCmdSetScissor( VkCommandBuffer commandBuffer, uint32_t firstScissor, uint32_t scissorCount, const VkRect2D* pScissors);
绘制指令
绘制指令可以帮助我们对图元进行装配。Vulkan 支持索引或者非索引形式的绘制指令。绘制指令渲染到帧缓存的顺序与片元的顺序一致。如果当前有多个绘制指令的实例正在使用,那么绘制指令的执行顺序与 API 的顺序也一致。对于非索引形式的指令,位置更靠前的实例所包含的图元会优先进行处理。而对于基于索引的指令,API 会优先选择顶点索引值更靠前的图元进行处理。
Vulkan 可以将绘制指令录制到指令缓存中,从而实现场景物体的绘制。Vulkan 中支持四种不同的绘制指令,它们通常可以被分为两大类别,如下所示:
第一种类型(vkCmdDraw 和 vkCmdDrawIndexed)直接将绘制所用的参数包含在指令缓存对象中。
与之对应的第二种类型(vkCmdDrawIndirect 和 vkCmdDrawIndexedIndirect)使用缓存内存来实现绘制函数参数的间接读取,因此这里的函数名都带有 Indirect 关键字,否则属于第一种类型的函数。
vkCmdDraw —— 渲染几何体
API 函数 vkCmdDraw() 直接从指令缓存中读取绘制参数。顶点信息按顺序从一个数组中读入,其中第一个顶点通过参数 firstVertex 设置,而顶点的总数通过参数 vertexCount 设置。函数 vkCmdDraw 通过上述的输入装配状态(即流水线状态对象中的顶点数组)来实现图元的渲染。
这个函数支持实例化,因此如果我们多次调用渲染指令,对同一个对象进行多次绘制时,可以更高效地完成工作。这种绘制特性对于大规模群集渲染、数目渲染、铺陈样式图案等情形都是非常有帮助的。绘制实例的总数可以通过参数 instanceCount 来设置,而第一个实例的索引号可以通过参数 firstInstance 进行设置。
VKAPI_ATTR void VKAPI_CALL vkCmdDraw( VkCommandBuffer commandBuffer, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance);
vkCmdDrawIndexed —— 渲染索引几何体
它主要被用来绘制索引几何体。函数 vkCmdDrawIndexed() 可以通过索引缓存来执行绘制指令。索引缓存中的每个顶点都被表示为一个索引数字。这种表示网格数据的方式需要更少的内存空间核磁盘空间,可以有效利用网格中共享的顶点来表示连接关系(例如闭合的形状)。
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexed( VkCommandBuffer commandBuffer, // 索引列表中的索引总数 uint32_t indexCount, // 设置使用该指令进行绘制的实例总数 uint32_t instanceCount, // 设置索引数组中的第一个索引值的位置 uint32_t firstIndex, // 设置一个顶点索引的偏移值,由此计算得到的新索引值会被用来实际读取顶点缓存中的顶点 int32_t vertexOffset, // 设置准备绘制的第一个实例的 ID uint32_t firstInstance);
例如,一个四边形几何体是由两个三角形组成的,其中有两个顶点是被这两个三角形共享的,如下面的示例代码所示,其中第一个和第三个顶点是重复的:
struct VertexWithColor { float x, y, z, w; // Vertex Position float r, g, b, a; // Color format Red, Green, Blue, Alpha }; 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 }, }; // 6 个顶点索引,对应两个三角形,即绘制一个矩形(实际是正方形) uint16_t squareIndices[] = { 0,3,1, 3,2,1 }; // 6 indices
顶点位置和对应的颜色,以及三角形绘制路线:
实际运行的渲染结果:
以下是实现索引绘制的基本步骤:
- 使用 squareData 和 squareIndices 来创建一个缓存资源(VkBuffer)。将这个 VkBuffer 的句柄保存到 VertexBuffer::buf 以及 VertexIndex::idx 当中。
- 使用 vkCmdBindVertexBuffers() 绑定顶点缓存,将 VertexBuffer.buf 作为参数传递进去。
- 与之类似,使用函数 vkCmdBindIndexBuffer() 绑定索引缓存 VertexIndex.idx。
- 使用 vkCmdDrawIndexed() 绘制这个对象。
渲染当前可绘制对象
当完成了渲染通道实例的准备工作,成功地将绘制对象的指令存储到指令缓存当中之后,我们就可以每次重新使用这些指令来实现物体的绘制了。
对象的绘制包括了三个步骤:首先,我们需要获取下一幅可用的交换链图像的索引号,以便将图元绘制上去并完成光栅化。其次,我们需要提交指令缓存到图形队列中,在 GPU 端执行之前录制的指令。GPU 执行指令之后会将数据绘制到当前的交换链图像之上。最后,将绘制完成的图像传递给展示引擎进行处理,也就是将输出结果渲染到对应的显示窗口中。
vkAcquireNextImageKHR —— 获取展示图像的索引
VKAPI_ATTR VkResult VKAPI_CALL vkAcquireNextImageKHR( VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout, VkSemaphore semaphore, VkFence fence, uint32_t* pImageIndex);
vkQueuePresentKHR —— 使用展示引擎显示输出内容
当系统执行可绘制对象的指令缓存时,它将使用录制好的指令在目标展示图像上进行渲染。之后这幅图像将通过 API 函数 vkQueuePresentKHR() 提交给展示引擎,并且将展示图像输出到显示设备端。
VKAPI_ATTR VkResult VKAPI_CALL vkQueuePresentKHR( VkQueue queue, const VkPresentInfoKHR* pPresentInfo);
- 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;
理解Vulkan中的同步图元
同步是一个异步系统中保持顺序和规则的核心因素。它不仅可以提升资源的统一性,还可以辅助并行的机制来降低 CPU 和 GPU 的空闲时间损耗。
Vulkan 提供了以下四种类型的同步图元来实现并行的代码执行:
- 栅栏(Fence):提供宿主机和设备之间的同步机制。
- 信号量(Semaphore):提供队列之间,以及队列内的同步机制。
- 事件(Event):队列提交时的同步。
- 屏障(Barrier):指令缓存中各个指令之间的同步。
vkCreateFence —— 创建栅栏对象
VKAPI_ATTR VkResult VKAPI_CALL vkCreateFence( VkDevice device, const VkFenceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkFence* pFence);
- VkFenceCreateInfo
typedef struct VkFenceCreateInfo { VkStructureType sType; const void* pNext; VkFenceCreateFlags flags; } VkFenceCreateInfo; typedef enum VkFenceCreateFlagBits { VK_FENCE_CREATE_SIGNALED_BIT = 0x00000001, VK_FENCE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkFenceCreateFlagBits; typedef VkFlags VkFenceCreateFlags;
vkWaitForFences —— 等待栅栏对象
VKAPI_ATTR VkResult VKAPI_CALL vkWaitForFences( VkDevice device, uint32_t fenceCount, const VkFence* pFences, VkBool32 waitAll, uint64_t timeout);
vkDestroyFence —— 销毁栅栏对象
VKAPI_ATTR void VKAPI_CALL vkDestroyFence( VkDevice device, VkFence fence, const VkAllocationCallbacks* pAllocator);
vkResetFences —— 重设栅栏对象
VKAPI_ATTR VkResult VKAPI_CALL vkResetFences( VkDevice device, uint32_t fenceCount, const VkFence* pFences);
vkCreateSemaphore —— 创建信号量对象
VKAPI_ATTR VkResult VKAPI_CALL vkCreateSemaphore( VkDevice device, const VkSemaphoreCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSemaphore* pSemaphore);
- VkSemaphoreCreateInfo
typedef struct VkSemaphoreCreateInfo { VkStructureType sType; const void* pNext; VkSemaphoreCreateFlags flags; } VkSemaphoreCreateInfo; typedef enum VkFenceCreateFlagBits { VK_FENCE_CREATE_SIGNALED_BIT = 0x00000001, VK_FENCE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkFenceCreateFlagBits; typedef VkFlags VkFenceCreateFlags; typedef VkFlags VkSemaphoreCreateFlags;
vkDestroySemaphore —— 销毁信号量
VKAPI_ATTR void VKAPI_CALL vkDestroySemaphore( VkDevice device, VkSemaphore semaphore, const VkAllocationCallbacks* pAllocator);
vkCreateEvent —— 创建事件对象
VKAPI_ATTR VkResult VKAPI_CALL vkCreateEvent( VkDevice device, const VkEventCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkEvent* pEvent);
- VkEventCreateInfo
typedef struct VkEventCreateInfo { VkStructureType sType; const void* pNext; VkEventCreateFlags flags; } VkEventCreateInfo; typedef enum VkEventCreateFlagBits { VK_EVENT_CREATE_DEVICE_ONLY_BIT = 0x00000001, VK_EVENT_CREATE_DEVICE_ONLY_BIT_KHR = VK_EVENT_CREATE_DEVICE_ONLY_BIT, VK_EVENT_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkEventCreateFlagBits; typedef VkFlags VkEventCreateFlags;
vkDestroyEvent —— 销毁事件对象
VKAPI_ATTR void VKAPI_CALL vkDestroyEvent( VkDevice device, VkEvent event, const VkAllocationCallbacks* pAllocator);
vkGetEventStatus —— 查询事件状态
VKAPI_ATTR VkResult VKAPI_CALL vkGetEventStatus( VkDevice device, VkEvent event);
函数返回值为 VK_EVENT_SET,表示事件已经收到了信号;如果事件没有收到信号,返回值是 VK_EVENT_RESET。
vkSetEvent —— 设置事件
VKAPI_ATTR VkResult VKAPI_CALL vkSetEvent( VkDevice device, VkEvent event);
vkResetEvent —— 重设事件
VKAPI_ATTR VkResult VKAPI_CALL vkResetEvent( VkDevice device, VkEvent event);
Vulkan 围炉夜话5:https://developer.aliyun.com/article/1598117