文章目录
第一个Vulkan程序,我们将构建自己的第一个Hello World的Vulkan程序。该程序使用伪代码形式的编程模型来编写。
初始化—与设备握手的过程
Vulkan
的初始化过程包括验证层属性的初始化,以及实例对象(VkInstance)的构建。当实例创建完成之后,需要检查当前系统是否存在可用的物理设备(VkPhysicalDevice)。选择可用的物理设备,并通过实例对象创建一个对应的逻辑设备(VkDevice)。在Vulkan程序当中,逻辑设备对象会被大多数的API所使用,它可以被视为是当前物理设备的一个逻辑表示。如图所示。
Vulkan通过错误信息和验证层提供了调试功能。这类功能扩展主要有如图所示的两种类型:
- 实例相关(
Instance-specific
):提供了全局级别的扩展。 - 设备相关(
Device-specific
):提供了物理设备相关的扩展。
在程序开始的时候,系统会枚举所有的全局层和设备相关的功能扩展(来自Vulkan驱动)。全局层和扩展可以直接注入实例对象中,并且在全局的层面启用。不过,只在设备层面启用扩展函数将只能在指定的设备上启用该功能。
初始化的过程中需要创建实例和设备对象。此外,还需要查询所有的全局层/功能扩展,并且从全局或者实例对象层面启用。类似地,我们也可以在特定设备上启用功能扩展。以下给出了初始化过程所需的伪代码。
1)枚举实例层属性:Vulkan首先要和加载器进行通信并定位驱动位置。驱动中包含了多个功能扩展和层对象,对于不同的GPU供应商,这些对象可能也有所不同。vkEnumerate InstanceLayerProperties函数返回层的数量及其属性。每个层都包含了多个功能扩展,可以直接通过vkEnumerateInstanceExtensionProperties函数进行查询。
2)创建实例:实例对象(VkInstance)的创建需要使用vkCreateInstance()这一API函数,参数设置为层的名称,以及期望开启(以便进行验证或者调试)的功能扩展。这些名称是通过VkInstanceCreateInfo结构体来设置的。
3)创建设备:枚举当前系统中所有的物理设备或者GPU的数量,并且调用vkEnumerate PhysicalDevices()API函数。
对于每个物理设备来说,如果需要在创建实例的过程中枚举设备相关的功能扩展,所用的方法也是一样的。
我们需要使用vkEnumerateInstanceLayerProperties和vkEnumerateInstanceExtensionProperties这样的API,基于已有的实例来枚举功能。而基于设备来进行功能枚举的函数已经被废弃了,因此,我们需要使用vkEnumerateDeviceExtensionProperties来枚举扩展功能。
当我们已经获取了物理设备的列表之后,可以查询以下设备信息:
- 队列和队列的类型:使用vkGetPhysicalDeviceQueueFamilyPropertiesAPI函数查询物理设备中的队列以及队列属性。我们还可以从查询得到的队列中搜索图形相关的队列,将它的队列索引号保存到应用程序中,稍后使用。之所以要在这里搜索图形队列,是因为我们在本文中只想进行绘制操作。
- 内存信息:API函数vkGetPhysicalDeviceMemoryProperties()可以用来获取给定物理设备中可用的内存类型。
- 物理设备属性:用户可以自由选择将物理设备的属性保存起来,以便在后续编程的过程中获取某些特定的属性。这一操作可以通过API函数vkGetPhysicalDeviceProperties()来完成。
设备对象的创建是通过函数vkCreateDevice()完成的。它是物理设备在应用程序层面的逻辑表示形式。应用程序可以在不同的地方直接使用设备对象:
下图中总结了所有创建Vulkan实例和设备的方法,读者可以据此对整个创建过程做一次快速的回顾。
交换链初始化—查询WSI扩展
展示层负责将已经渲染完成的内容显示到输出窗口中。因此我们需要创建一个空窗口,以便将绘制的结果图像粘贴到窗口中。创建空窗口的方法是使用API函数CreateWindowEx(Windows平台)或者xcb_create_window(Linux平台)。
我们首先需要使用实例和设备的WSI扩展函数来初始化展示层。这些API函数允许用户使用多种不同的表面属性来创建展示表面。
这些API必须动态地链接到程序中,并且保存为函数指针的形式。我们可以使用API函数vkGetInstanceProcAddr()来进行查询,如表所示。
使用已有实例来执行的扩展函数API如下所示。
相应地,使用设备作为参数来执行的扩展函数API见表。
这些API是我们执行图像展示相关的功能需求所必不可少的。我们看一看还有什么操作是必须完成的:
- 创建一个抽象的表面对象:创建绘制表面首先要创建VkSurfaceKHR对象。这个对象可以将本地系统平台(Windows、Linux、Wayland、Android等)的窗口/表面功能机制抽象出来。创建这个对象需要调用API函数
vkCreate<Win32/Wayland/Android>SurfaceKHR()
。 - 在展示层中使用图形队列:我们可以使用创建后的抽象表面对象,搜索可用于当前展示层的图形队列,对应的API函数为vkGetPhysicalDeviceSurfaceSupportKHR()。
我们可以将搜索到的队列的句柄或者索引号保存下来。然后用它来查询表面的属性,创建对应该队列的逻辑对象(下一步的工作)。
- 获取兼容的队列:在开始记录任何一种指令缓存之前,我们都需要获取对应的队列来完成指令的收取操作。我们可以使用函数vkGetDeviceQueue()并指定可用队列的句柄或者索引号,而这个句柄或者索引号是我们在上一步中获得并保存的。
- 查询表面格式:获取给定物理设备所支持的所有的绘制表面格式,可以通过函数vkGetPhysicalDeviceSurfaceFormatsKHR实现:
下图给出了一个有关展示层初始化过程的快速概览。
1.指令缓存初始化——分配指令缓存
在创建一个展示表面之前,我们需要用到指令缓存。指令缓存负责记录指令并发送到一个对应的队列中进行处理。
指令缓存的初始化过程主要包括以下步骤:
- 指令池的创建:还记得吗?我们已经保存了当前展示层可用的图形队列的句柄信息。现在可以使用索引号或者句柄来创建一个指令池了,对应的API函数是vkCreateCommandPool(),它与其他的查询函数无异。
- 分配指令缓存:当我们有了指令池之后,指令缓存的分配可以直接通过函数vkAllocateCommandBuffers()来完成。
我们不需要每帧都从指令池当中创建新的指令缓存,即使它是需要被反复使用的。而如果一个已有的指令缓存已经不再被使用了,我们也可以将它高效地直接复用。
如图所示,指令缓存池可以用来指定一块内存区域,在不进行全局同步的前提下直接创建新的指令缓存。
2.资源对象——管理图像和缓存
理解Vulkan的资源类型的基本概念是非常重要的。资源管理包括资源的创建、分配和绑定。例如,展示表面本身就会把绘制表面当作一种常见的Vulkan资源类型使用。
Vulkan将资源分为两种类型:缓存和图像,如图所示。
这些资源还可以进一步划分为不同的视图,具体如下:
- 缓存:缓存对象使用线性的数组类型来表示不同的资源。缓存对象的类型是VkBuffer,可以通过API函数vkCreateBuffer()创建。这个函数需要输入一个VkBufferCreateInfo结构体作为参数,它设置了对象创建过程中可能会用到的各种不同的属性参数。例如,用户可以设置图像平铺的数量、图像的用途、大小、队列兼容性,等等。我们现在再了解一下缓存视图是如何被建立的。
- 缓存视图:缓存视图(VkBufferView)表示数据缓存自身。它可以将数据用连续的方式保存起来,并设置一个特定的数据解析格式。创建数据视图时需要用到函数vkCreateBufferView()。它需要一个VkBufferViewCreateInfo结构体作为输入,以设置各种缓存相关的属性,例如所用的缓存对象(VkBuffer)格式、缓存视图的范围,等等。
- 图像:我们在程序中使用VkImage来表示它。这个对象可以保存一维到三维的缓存数组。对象的创建是通过函数vkCreateImage()完成的。与缓存对象类似,这里也需要用到一个VkImageCreateInfo结构体来设置创建对象时可能用到的各种属性。现在我们再了解一下图像视图是什么。
- 图像视图:与缓存视图类似,图像视图对象的类型是VkImageView。我们需要使用结构体VkImageViewCreateInfo和vkCreateImageView()API来创建一个图像视图对象。
应用程序中并不会直接访问缓存(VkBuffer)和图像(VkImage)对象,而是使用对应的视图对象(VkBufferView以及VkImageView)来完成。
3.创建展示表面——创建交换链
我们快速回顾一下。首先,我们已经创建了一个Vulkan的实例,它是一个用来表示实际物理设备的逻辑对象,我们还查询了可用作展示的队列属性,将它的索引号保存下来备用。我们还创建了WSI扩展的函数指针,并且了解了Vulkan的资源类型。最后我们还从指令池当中初始化并且创建了指令缓存。
下文中将详细描述我们该如何启用指令缓存记录过程。
指令缓存中应当记录什么内容?
a)为交换链构建要绘制的图像以及深度图像,并进行深度/模板测试。
b)创建着色器模块,并关联到着色器程序代码。
c)构建着色器所需的资源,它需要一个描述符集以及流水线布局。
d)创建并管理渲染通道和帧缓存对象。
e)执行绘制操作。
我们需要调用API函数vkBeginCommandBuffer()来启动指令缓存的录制工作。它定义了指令缓存的起始范围。之后,任何新 增的指令都会被记录到指令缓存当中。
现在我们将了解交换链的创建过程。我们总是需要从交换链中获取图像,然后对它执行渲染操作:
1)获取表面性能参数:查询表面性能参数,例如当前尺寸、最大/最小的可用尺寸、空间变换的性能,等等,都可以通过vkGetPhysicalDeviceSurfaceCapabilitiesKHR()函数完成。
2)获取表面展示模式:展示模式的作用是表示当前绘制表面所用的更新方法,例如,它是否可以以立即模式进行渲染,或者支持垂直回扫,等等。展示模式的获取可以通过API函数vkGetPhysicalDeviceSurfacePresentModesKHR()来完成。
3)创建交换链:我们可以使用表面性能参数结合展示模式来创建新的交换链对象。性能参数,以及其他一些常见属性例如尺寸、表面格式等,都需要在VkSwap ChainCreateInfo结构体中指定并作为参数传递到vkCreateSwapchainKHR()来创建新的对象。
4)获取交换链的图像:我们可以通过API函数vkGetSwapchainImagesKHR来查询交换链中支持的图像表面的数量,并且获取对应的图像对象(VkImage)。例如,如果交换链支持双重缓冲的话,那么这里返回的数量值应该是2,并且我们可以获取2幅图像以用于绘制。
交换链中的图像并不会在应用程序层面分配更多的内存空间。交换链内部会负责管理内存的分配以及返回结果对象。应用程序只需要决定如何从图像视图获取图像并使用即可。这里的图像视图描述了图像的使用方法。
5)设置图像布局:每一幅图像都可以设置一个与具体设备实现兼容的布局方式,并添加一个流水线的线程屏障。根据Vulkan的标准所述,流水线屏障可以插入一个执行依赖,以及在指令缓存的一组指令中间插入一组内存依赖,插入顺序是先插入到指令缓存本身,然后插入到一组指令集当中。这一过程可以通过函数vkCmdPipelineBarrier来完成。插入屏障之后,系统就可以确保指定布局中的图像视图在被应用程序调用之前,肯定已经处于可用的状态了。
6)创建图像视图:应用程序只会使用VkImageView对象,而创建VkImageView对象需要用到vkCreateImageView方法。我们需要自己将视图对象保存到应用程序中使用:
下图中给出了交换链图像对象(VkImage)作为图像视图对象(VkImageView)使用的整个过程。
4.创建深度图像
应用程序需要使用深度图像来完成深度测试的工作。不过对于2D绘制的逻辑而言,只使用交换链的图像就已经足够了。深度图像创建的过程与交换链图像是一样的。不过两者之间还是有一点不同:交换链图像是内部创建好可以直接使用的(从vkGetPhysicalDeviceSurfaceFormatsKHR()直接返回),而深度图像对象(VkImage)的内存分配和创建过程都是在应用程序中手动控制的。
下面给出了深度图像的创建过程:
1)首先,我们使用API函数vkGetPhysicalDeviceFormatProperties()来查询深度图像所用到的物理设备的格式属性。
2)使用API函数vkCreateImage创建图像对象,并且通过API函数vkGetImageMemory Requirements获取资源的内存需求情况。
3)下一步,根据资源对内存的需求属性,使用函数vkAllocateMemory()分配内存。将分配好的内存绑定到新创建的图像对象,创建函数为vkBindImageMemory。
4)和交换链中的绘制图像类似,我们也可以设置深度图像的布局,创建图像视图以便后续使用。
如图所示,新分配的深度图像(VkImage)被关联到对应的视图类型(VKImageView),后者的对象也需要分配到内存中。
下面的伪代码给出了深度图像对象创建的整个过程。这里的深度图像主要是用于深度测试:
5.资源分配——分配和绑定设备内存
Vulkan的资源(缓存为VkBuffer,图像为VkImage)刚刚被创建时,并没有关联任何的内存空间。因此在我们使用某个资源之前,必须先为它分配内存空间,并且将资源绑定到该内存。
为Vulkan资源对象分配内存的时候,首先由应用程序端查询物理设备上可用的内存空间,对应的函数为vkGetPhysicalDeviceMemoryProperties。这个API函数可以支持一个或者多个内存堆,并且在堆的基础上提供一个或者多个内存类型。这些可用的属性被存储在一个负责控制内存的结构体VkPhysicalDeviceMemoryProperties中。对于一个通常的PC用户来说,内存堆的种类有两种:系统RAM以及GPU RAM。此外,每个内存堆都可以进一步分为多种内存类型。
内存的属性查询,例如堆类型的查询,可以在应用程序初始化阶段完成,然后将它保存到应用程序变量中,稍后使用。
每种内存类型都支持多种物理设备属性,可以直接查询。例如,有的内存类型可以被CPU识别,有些不行,这事关CPU和GPU访问的一致性,有的可以缓冲,有的不能缓冲。这类查询允许应用程序先选择正确合用的内存类型,然后再执行Vulkan的通常过程,进行资源的分配:
- 内存需求:资源对象(VkBuffer和VkImage)的创建与它们的对象属性有关,例如纹理排列模式、用法的标记,等等。每种对象都有自己的内存创建需求,可以通过vkGetBufferMemoryRequirements或者vkGetImageMemoryRequirements来进行查询。这个函数有助于计算内存分配的大小,例如,它返回的值可能需要考虑边界对齐等因素。函数内部会兼顾到内存类型和对应的位格式,确保它与资源对象是兼容的。
- 分配:内存的分配需要用到API函数vkAllocateMemory。它的输入参数包括设备对象(VkDevice)以及内存控制的结构体(VkPhysicalDeviceMemoryProperties)。
- 绑定:我们根据自己的内存需求,找到了合适的内存类型之后,就可以分配内存空间了。然后我们将资源对象绑定到这个刚分配的内存上,对应的函数为vkBindBufferMemory或者vkBindImageMemory。
- 内存映射:内存映射的职责是更新物理设备内存数据。首先,使用函数vkMap Memory将设备内存映射到宿主机内存。然后更新映射内存区域(在宿主机上)的数据,最后调用vkUnmapMemory函数。这个API函数会将映射内存区域的数据更新到设备内存中。
着色器支持—将着色器编译到SPIR-V
我们可以使用glslangValidator.exe(LunarG SDK提供的工具)编译着色器文件,并将它们从可读的文本格式转换到SPIR-V格式,即二进制的中间文件格式,可以直接被Vulkan识别:
下面的伪代码给出了应用程序中创建着色器模块的过程。要创建某个着色器类型(顶点、片元、几何、细分,等等)对应的着色器模块,可以调用函数vkCreateShader Module。调用时必须使用SPIR-V格式的中间二进制着色器代码,它是在控制参数结构体VkShaderModuleCreateInfo中定义的:
构建布局—描述符与流水线布局
描述符负责将资源与着色器通过布局绑定的方式关联起来。我们通常用它来绑定一致和采样器变量到着色器中。
一个描述符集里可以包含多个布局绑定的描述符,它们在代码中的表现形式相当于是数组的代码块,如下文的伪代码所示。这些代码块需要被传递给一个单独的控制参数结构体VkDescriptorSetLayoutCreateInfo,然后再调用vkCreateDescriptorSetLayout函数来创建描述符对象。一个描述符集的布局表示这个描述符集所包含的信息的类型。
描述符被创建之后,还不能够直接被底层的流水线访问到。我们必须创建了流水线布局之后才能够实现访问。流水线布局的意思就是让底层流水线可以访问到描述符集的信息。我们需要使用函数vkCreatePipelineLayout来创建它,输入参数包括一个VkPipelineLayoutCreateInfo控制参数结构体对象,用以记录描述符布局的信息:
这个示例只用到了属性信息(顶点位置和颜色)。它没有使用任何的一致变量或者采样器。因此,我们在这里并不需要定义对应的描述符。
创建渲染通道—定义通道属性
下一步我们要创建渲染通道对象。渲染通道包括子通道和附件。它描述了绘制操作的结构、数据在各个附件之间流转的方式以及顺序的要求。此外还有运行时的一些定义,例如附件加载时的行为方式,是否需要清除帧缓存还是保留原有信息。渲染通道的创建是通过调用vkCreateRenderPass函数完成的。它的输入参数包括子通道和附件的控制参数结构体。下面的伪代码为我们提供了更多的信息:
帧缓存—将绘制图像关联到渲染通道
帧缓存指的是图像视图的一个集合,它的内容与渲染通道中定义的各个附件是一一对应的。图像视图中包含了绘制图像或者深度图像。而渲染通道对象被用来控制这些附件,并且在创建渲染通道的时候指定附件的各种属性。
控制参数结构体VkFramebufferCreateInfo中可以保存渲染通道对象、附件,以及其他一些重要的参数,例如图像尺寸、附件的数量、层,等等。我们将这个结构体传递给VkCreateFramebuffer函数来创建帧缓存对象。
我们使用附件来表示颜色和深度缓存,它必须是图像视图(VKImageView)的形式,而不是图像对象(VkImage)。
如图所示为创建帧缓存对象的过程。其中包含了图像视图(用作绘制的颜色缓存图像)和深度视图(用作深度测试)。
我们通过下面的伪代码了解一下帧缓存的创建过程:
产生几何体—在GPU内存中保存一个顶点
下一步,我们将定义一个几何形状,然后将它显示在输出设备上。我们会显示一个简单的三色三角形。
如图中所示就是三角形所需的几何数据。其中包含了顶点的位置,接着是每个顶点的颜色信息。这个数据数组需要通过Vulkan的缓存对象(VkBuffer)传递给底层的物理设备。
下面的伪代码涵盖了这个缓存对象的分配,映射和绑定的过程:
创建缓存资源的过程与创建图像对象非常类似。Vulkan提供了一系列缓存相关的API函数,支持内存分配、映射、绑定等操作。它们与图像对象管理的API很相似。缓存和图像资源的管理函数以及对应的数据结构的对比见表。
缓存一开始并不会和任何形式的内存空间向关联。应用程序必须主动分配和绑定设备内存到缓存对象,然后再使用它。与图像对象不同的是,缓存对象可以直接被使用(例如顶点属性、一致变量,等等),而图像是强制在图像视图内部创建完成然后交由用户使用的。如果在着色器阶段访问缓存对象的话,我们需要通过缓存视图对象的形式去访问它。如图所示。
当我们将顶点数据上传到设备内存之后,还需要将数据的格式标准告知流水线。这对于数据的获取和解析都是很有帮助的。例如,用户提供的几何体顶点数据可能包含了位置和颜色的信息,通过连续存储的方式保存,每个属性的大小为16字节。这些信息都需要传递到底层流水线端,可以通过顶点输入绑定描述符(VkVertexInputBindingDescription)以及顶点输入属性描述符(VkVertexInputAttributeDescription)两个控制参数结构体来完成。
- VkVertexInputBindingDescription中的属性可以帮助流水线读取缓存资源数据,例如数据中每个单元的间距,这里还要考虑到数据读取的频率(是逐顶点读取还是基于一系列的实例)。
- VkVertexInputAttributeDescription负责缓存资源数据的解析工作。
在下面的伪代码中,位置和颜色属性分别在0和1位置进行读取。因为数据是连续存储的,因此它们的字节偏移分别是0和16:
我们在创建流水线的时候用到了结构体对象viAttribs和viBinding。流水线对象中包含了状态信息,其中就有顶点输入的状态,它们对于缓存资源的读取和解析是非常有帮助的。
流水线状态管理—创建流水线
流水线是一系列状态的集合。每个状态都包括了一组属性,用来定义状态的执行协议。这些状态组合在一起就构成了一条流水线。流水线的类型有两种:
- 图形流水线:这类流水线包含了多个着色器阶段,包括顶点、片元、细分、几何,等等。它包括了一个流水线布局和多个固定函数的流水线阶段。
- 计算流水线:它主要用作各种计算操作。其中包含了一个静态的计算着色器阶段,以及对应的流水线布局。
流水线阶段的管理主要分为两个阶段。第一个阶段包括定义多个状态对象,其中有一些重要的状态控制属性。第二个阶段则需要使用这些状态对象来创建流水线对象。
1.定义状态
一个流水线中可能包括了多个状态,这些状态的定义如下:
- 动态状态:动态状态会提示流水线在运行过程中观察状态的变化情况。此时流水线会使用一个特殊的过程去更新各个状态量,而不是直接使用初始值。例如,视口和裁切属性都是动态状态。在用户应用程序中使用结构体VkPipelineDynamic StateCreateInfo可以设置动态状态以及它们的属性。
- 顶点输入状态:这个状态帮助流水线读取和解析用户数据。使用VkPipelineVertexInputStateCreateInfo对象来设置内部的顶点输入绑定(VkVertexInputBindingDescription)以及顶点输入属性(VkVertexInputAttributeDescription)描述符。
- 光栅化状态:在这个过程中,图元被转换为二维图像中的关键信息,例如颜色、深度等属性数值。这个状态是通过结构体VkPipelineRasterizationStateCreateInfo表示的。这个结构体可以设置裁减模式、正面方向、图元类型、线宽度等各种属性。
- 颜色融混附件状态:融混是源颜色数据和目标颜色数据的一种组合。组合的方式有很多种,需要通过不同的属性和融混公式来计算。这里使用结构体VkPipelineColorBlendStateCreateInfo来进行表示。
- 视口状态:这个状态对于控制视口变换是很有用处的。视口属性可以通过VkPipelineViewportStateCreateInfo进行设置。视口可以有多个。这个状态也可以帮助我们查询给定视口的一些关键属性,例如尺寸、起始点、深度范围等等。对于每一个视口来说,都有一个与之对应的剪切矩形,用来完成剪切测试的边界定义。
- 深度模板状态:通过结构体VkPipelineDepthStencilStateCreateInfo可以实现对于深度边界测试,模板测试以及深度测试的属性控制。
- 多重采样状态:多重采样状态包含了一些重要的属性,可以控制Vulkan图元(点、线、多边形)进行光栅化时,反走样机制的实现。结构体VkPipelineMultisampleStateCreateInfo负责设置这些相关的控制属性。
下面的伪代码定义了不同的流水线状态对象,并且最终创建了一个图形流水线:
2.创建图形流水线
流水线状态对象被封装在结构体VkGraphicsPipelineCreateInfo当中。这个结构体允许我们直接访问图形流水线对象中的流水线状态信息。
流水线状态对象的创建过程是非常耗费时间的。要提升整体性能的话这也是关键的路径之一。因此,我们需要从流水线缓存(VkPipelineCache)中构建流水线状态对象来获得最大的性能。这样驱动端就可以直接使用已有的基础流水线数据来构建一个新的流水线。
图形流水线对象的创建可以通过函数vkCreateGraphicsPipelines来完成。这个函数可以使用流水线缓存对象来分配新的VkPipeline对象,并且通过VkGraphicsPipelineCreateInfo对象来设置这个流水线对应的所有状态:
执行渲染通道—显示Hello World!!!
我们已经快要接近终点了!现在我们会尝试渲染我们的简单三角形,通过渲染通道的辅助将它输出到绘制表面上。渲染通道的执行需要一个绘制表面,以及记录一系列的指令来定义渲染通道执行的内容。
1.获取绘制表面
在我们开始渲染之前,第一件必须要做的事情就是获取绘制用的帧缓存。我们已经创建了帧缓存对象,并且将交换链中的绘制图像(来自交换链的图像视图)与它关联起来。现在我们将使用vkAcquireNextImageKHR函数来获取当前绘制操作可以使用的图像的索引号。通过这个索引号,我们将获取对应的帧缓存并且将它传递给渲染通道,实现几何图形的渲染:
这里我们需要用到同步的机制,因为可能有两个或者更多交换链的图像正在被使用。绘制图像的获取操作必须在它已经被渲染到显示输出设备之后,并且已经准备完毕的前提下才能完成。这个准备状态可以通过vkAcquireNextImageKHR来判断。信号量对象在这里被用来实现绘制图像的获取操作的同步。信号量(VkSemaphore)可以使用函数vkCreateSemaphore来创建,然后在指令缓存提交的时候使用。
2.准备渲染通道的控制参数结构体
渲染通道还需要设置一些信息,比如帧缓存、渲染通道对象、渲染区域的尺寸、清屏颜色、深度模板值,等等。这些信息的设置可以通过控制参数结构体VkRenderPassBeginInfo完成。这个结构体会被用于定义渲染通道的执行方式。下面的伪代码可以帮助你进一步理解这个结构体的内容:
3.渲染通道的执行
渲染通道的执行范围是在用户自定义的代码中完成的。这段自定义代码解析的起始和结束标志位需要通过API函数vkCmdBeginRenderPass和vkCmdEndRenderPass分别进行定义。在这个范围标志位中间的指令会被自动链接到渲染通道:
1)绑定流水线:使用函数vkCmdBindPipeline绑定图形流水线。
2)绑定几何体缓存:如果要设置顶点数据缓存对象(类型为VkBuffer)并传递给渲染通道,可以使用函数vkCmdBindVertexBuffers。
3)视口和剪切:我们可以调用函数vkCmdSetViewport和vkCmdSetScissor来设置视口和剪切矩形的尺寸。
4)绘制对象:通过绘制指令来设置更多的信息,例如需要读取的顶点数量、实例的数量,等等。
在完成指令缓存的记录之前,我们还需要设置一个兼容本机驱动的图像布局,并且调用vkEndCommandBuffer来结束指令缓存的录制:
如图所示为渲染通道的执行过程。这里特别指出了渲染通道范围内执行的各个操作。
队列的提交和同步—发送任务
最后一步了,我们已经成功地记录了多个指令到指令缓存中,其中包括了渲染通道的信息和图形流水线对象。下一步我们需要将指令缓存发送到队列。驱动程序会开始读取指令缓存并且准备调度和执行。
为了确保高效地渲染,指令缓存会被打包为多个绘制批次。因此,如果我们有多个指令缓存的话,我们需要设法将它们打包到一个VkCommandBuffer数组中。
在发送指令缓存之前,我们必须提前了解之前发送的批次的执行状态。如果之前的批次已经处理完成了,我们就可以向队列中推送一个新的批次。Vulkan提供了屏障(VkFence)机制来实现同步,以便确保之前的发送任务已经完成了。屏障对象(VkFence)可以通过函数vkCreateFence来创建。这个函数的输入参数中包括了一个VkFenceCreateInfo控制结构体。
指令缓存需要传递给一个发送给对象(VkSubmitInfo)。这个对象中包括了一个指令缓存列表,以及一个VkSemaphore对象来实现帧缓存和交换链图像的同步。这些信息被进一步传递给函数vkQueueSubmit,其中包含了一个VkQueue对象来包含发送的指令缓存数据,以及一个VkFence对象来确保每个指令缓存的发送之间可以正确同步:
使用展示层进行显示—渲染三角形
当我们发送指令缓存到队列中之后,物理设备层面将采用异步的方式来进行处理。绘制的结果是渲染一个三色的三角形到交换链的绘制表面上。此时的表面对用户是不可见的,需要将它展示到显示窗口中。绘制表面的展示执行需要通过结构体VkPresentInfoKHR来设置参数。其中包含了展示的信息,例如应用程序中交换链的数量、可用的绘制表面图像的索引号,等等。这个控制参数结构体对象被作为vkQueuePresentKHR的输入参数使用。后者将当前的绘制表面图像切换到当前的显示窗口中。
当我们调用了vkQueueSubmit之后,展示队列需要等待一个imageAcquiredSemaphore信号量来记录最后一次发送完成的信号,然后再执行展示操作。
整合
我们简单地重现一下我们的第一个Vulkan伪代码程序。如图所示就是我们的整个工作模型。
首先,应用程序创建Vulkan的实例以及设备,初始化必要的层和扩展功能。设备包含了两种不同的队列(图形和计算),如图所示。队列负责收集指令缓存并发送它们到物理设备进行处理。
通过WSI扩展,我们可以设置绘制表面来执行图形内容的渲染。交换链可以将绘制表面作为图像来使用,但是必须采用图像视图的方式。与此类似,它也提供了深度图像视图。这些图像视图可以被帧缓存所使用。渲染通道则使用帧缓存来执行单元渲染的操作。
指令缓存是从指令缓存池中分配得到的,在渲染通道的执行过程中,它被用来记录各种指令。渲染通道的执行需要用到一些关键的Vulkan对象,例如图形流水线、描述符集合、着色器模块、流水线对象,以及几何体数据。
最后,指令缓存被发送到展示(图形)的队列。发送完成后,GPU层面会以异步的方式来处理指令。我们在这里可以使用多种同步的机制和内存屏障来确保渲染的过程正确。
酷客网相关文章:
评论前必须登录!
注册