首发于 Vulkan 学习指南
关注专栏 写文章
在前一章中,我们了解了 Vulkan 资源类型;我们了解图像资源(VkImage)是什么,以及如何在交换链图像中实现它们。 在本章中,我们将讨论第二种 Vulkan 资源,称为缓冲区资源(VkBuffer),并使用它们来准备一个简单的几何图形缓冲区。
本章还将介绍并实现一个渲染通道 Render Pass 和帧缓冲区 framebuffer。 渲染通道有助于组装一个工作单元。 它定义了附件以及与其关联的子通道(影响一个渲染作业)。 帧缓冲区消费创建的渲染通道并为每个对应的交换链图像创建单帧信息。 帧缓冲区将一组图像视图与 Render Pass 中描述的一组附件相关联。
另外,我们将使用 SPIR-V 在 Vulkan 中实现一个着色器,SPIR-V 是一种用于着色器和内核的二进制中间语言。
因此,我们将涵盖以下主题:
- 了解 Vulkan 缓冲区资源类型
- 使用缓冲区资源创建几何图形
- 了解渲染通道 Render Pass
- 使用渲染通道 Render Pass 并创建帧缓冲区 framebuffer
- 清除背景颜色
- 在 Vulkan 中使用着色器
了解 Vulkan 缓冲区资源类型
缓冲区资源以线性方式表示连续的数据阵列。 缓冲区资源通常用来存储属性数据信息,如顶点坐标,纹理坐标,关联颜色等。 Vulkan 中的缓冲区资源由 VkBuffer 对象表示,与视图形式(图像视图,VkImageView)表示的图像资源(VkImage)不同,缓冲区资源可以直接用作顶点数据的源,或者可以通过着色器利用描述符进行访问。 需要把它们显式转换为缓冲区视图(VkBufferView)以允许着色器以格式化的格式使用缓冲区数据的内容。 在本节中,我们将直接通过 API 命令来使用缓冲区资源。
首先,本节将讨论涉及 API 规范的缓冲区资源概念,以便在实现中使用它们。 接下来,我们会使用这些 API 并实现缓冲区资源来存储一个简单的三角形几何图形数据。 这个三角形会用在后续的章节中,用于把几何图形渲染到应用程序中进行演示。
创建缓冲区资源类型
缓冲区资源(VkBuffer)是使用 vkCreateBuffer API 创建的。 以下是语法:
VkResult vkCreateBuffer(
VkDevice device,
const VkBufferCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkBuffer* buffer);
此表描述了 vkCreateBuffer()API 的各个参数:
device :这是负责创建缓冲区资源的逻辑设备。
pCreateInfo:这指的是一个 VkBufferCreateInfo 指针;请查看以下部分以获取更多信息。
pAllocator :这控制着主机内存的分配过程。
buffer :当 VkBuffer 被创建后,返回它的指针。
这里定义了 VkBufferCreateInfo 语法:
typedef struct VkBufferCreateInfo {
VkStructureType type;
const void* pNext;
VkBufferCreateFlags flags;
VkDeviceSize size;
VkBufferUsageFlags usage;
VkSharingMode sharingMode;
uint32_t queueFamilyIndexCount;
const uint32_t* pQueueFamilyIndices;
} VkBufferCreateInfo;
下表介绍了 VkBufferCreateInfo 的各个字段:
type :这指定了该结构的类型;这必须是 VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO 类型。
pNext :这是一个扩展特定的结构。 它也可以是 NULL。
flags : 这些是 VkBufferCreateFlagBits 位域标志。 有关标志的更多信息,请参阅以下各节中的 VkBufferCreateFlagBits。
size :这是指要创建的缓冲区的总大小;大小以字节为单位指定。
usage :这是 VkBufferUsageFlagBits,用于指定描述缓冲区资源预期用法的位字段。 有关 VkBufferUsageFlagBits 的用法的更多信息,请参阅以下各节。
sharingMode : 这指定了缓冲区被多个队列族访问时的共享模式。 这必须是以下值之一:来自 VkSharingMode 的 VK_SHARING_MODE_EXCLUSIVE 或 VK_SHARING_MODE_CONCURRENT。
queueFamilyIndexCount :这表示 queueFamilyIndices 数组中的条目数。
pQueueFamilyIndices :这是一组要访问缓冲区的队列族数组。 共享模式必须是 VK_SHARING_MODE_CONCURRENT; 否则,就忽略。
销毁缓冲区
当不再需要缓冲区时,可以使用 vkDestroyBuffer()将其销毁:
void vkDestroyBuffer(
VkDevice device,
VkBuffer buffer,
const VkAllocationCallbacks* allocator);
该 API 接受三个参数,如下表所述:
device :这是销毁缓冲区对象的逻辑设备。
buffer :这是指需要销毁的 VkBuffer 对象。
pAllocator :这个参数控制着主机内存的释放过程;请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
创建缓冲区视图
使用 vkCreateBufferView()创建缓冲区视图。 以下是此 API 的语法:
VkResult vkCreateBufferView(
VkDevice device,
const VkBufferViewCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkBufferView* pView);
此表描述了 vkCreateBufferView 的各个参数:
device :这是创建图像缓冲区的逻辑设备的句柄。
pCreateInfo : 这是一个指向 VkCreateBufferViewInfo 的指针;控制着 VkBufferView 的创建。
pAllocator :这个参数控制着主机内存的分配过程;有关更多信息,请参阅第 5 章“Vulkan 中命令缓冲区以及内存管理”中的“主机内存”部分。
pView :这会返回创建的 VkBufferView 对象的句柄。
缓冲区视图是使用 vkCreateBufferView()API 创建的。 以下是语法信息:
typedef struct VkBufferViewCreateInfo {
VkStructureType type;
const void* pNext;
VkBufferViewCreateFlags flags;
VkBuffer buffer;
VkFormat format;
VkDeviceSize offset;
VkDeviceSize range;
} VkBufferViewCreateInfo;
该表描述了 VkBufferViewCreateInfo 的各个字段:
type :这是指结构的类型信息;它必须是类型 VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO
next :这是一个扩展特定的结构。 该参数也可以为 NULL。
flags :该参数保留供将来使用。
buffer :这是 VkBuffer 的句柄。
format :这指定了缓冲区数据元素的格式(VkFormat)。
offset :这用于在颜色 / 深度 / 模板被转换为颜色 color 分量后重新映射 color/depth/stencil。
range :这用于选择 mipmap levels 和 array layers 的范围,使其对该视图可以访问。
销毁缓冲区视图
使用 vkDestroBufferView()API 可以销毁缓冲区视图。 这个 API 使用了三个参数。 第一个指定负责销毁第二个参数指定的缓冲区视图的逻辑设备。 这是它的语法:
void vkDestroyBufferView(
VkDevice device,
VkBufferView bufferView,
VkAllocationCallbacks* pAllocator);
使用缓冲区资源创建几何图形
在本节中,我们将创建一个简单的几何形状 ----- 三角形。 它会在缓冲区资源的帮助下存储在 GPU 内存中。 大多数应用程序会通过 Uniformuniform 或存储 storage 块消耗缓冲区。 缓冲区资源的实现与图像资源非常相似,除了在此处不需要我们创建缓冲区视图(VkBufferView)。
准备几何图形数据
创建 MeshData.h 并定义其中的几何数据。 声明以下结构:
/*--------------------- MeshData.h */
// Mesh data structure and Vertex Data
struct VertexWithColor
{
float x, y, z, w; // Vertex Position
float r, g, b, a; // Color format Red, Green, Blue, Alpha
};
// Interleaved data containing position and color information
static const VertexWithColor triangleData[] =
{
{ 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0 },
{ 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0 },
{ -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0 },
};
几何图形的数据以交错方式进行存储,其中存储了三个顶点,使它们包含笛卡尔形式的顶点位置信息,接着是存储在 RGB 色彩空间中的颜色信息。 位置信息由四个部分组成:x,y,z 和 w。 颜色信息包含 r,g,b 和 a 分量。 下图显示了所得到的三角形:
创建顶点缓冲区
创建一个新的名为 VulkanDrawable 的用户自定义类。 这个类用来绘制期望的几何形状,在本例中是一个简单的三角形。 以下是声明用来创建和销毁缓冲区资源函数的头文件:
class VulkanDrawable
{
// Many lines skipped, please refer to the source code
public:
void createVertexBuffer(const void *vertexData, uint32_t dataSize, uint32_t dataStride, bool useTexture);
void destroyVertexBuffer();
// Structure storing vertex buffer metadata
struct {
VkBuffer buffer; VkDeviceMemory memory;
VkDescriptorBufferInfo bufferInfo;
} VertexBuffer;
// Stores the vertex input rate
VkVertexInputBindingDescription viIpBind;
// Store metadata helpful in data interpretation
VkVertexInputAttributeDescription viIpAttrb[2];
};
在这里,该类还包含一个特殊的用户自定义结构,用于聚合顶点缓冲区资源的属性,由 VertexBuffer 定义。 这个结构包含 VkBuffer 对象。该对象会被绑定到 VkDeviceMemory 和一个缓冲区描述符(VkDescriptorBufferInfo),这个缓冲区描述符(VkDescriptorBufferInfo)包含用于已分配缓冲区资源的必要元数据,例如缓冲区对象,其偏移量和范围。
缓冲区创建概述
缓冲区资源的创建过程与图像资源的创建过程非常相似。 要了解图像创建过程的描述,请参阅第 6 章“分配图像资源以及使用 WSI 构建交换链”中的“图像创建概述”部分。
让我们快速浏览一下缓冲区的创建。 以下是使用 Vulkan API 创建缓冲区资源(VkBuffer)的步骤说明:
- 创建缓冲区对象:使用 vkCreateBuffer()API 创建缓冲区对象(VkBuffer)。 此 API 会用到 VkCreateBufferInfo 结构对象,该对象指定了用于创建缓冲区对象的一些重要缓冲区元数据。 缓冲对象的 VkCreateBufferInfo 包含必要的内存信息,例如格式,用法,大小,创建标志等。 这个信息用于从设备分配物理内存。 您可以认为在此初始阶段的缓冲区对象没有后端的内存支持,即没有分配实际的物理存储。 创建缓冲区对象并不意味着物理分配在幕后自动完成;必须手动完成,这会在下一步中介绍。
- Allocating buffer memory 分配缓冲内存:
- 获取内存需求:使用 vkGetBufferMemoryRequirements()API 收集所需的内存信息。 在缓冲区资源的分配过程中,此信息有助于分配所需的适当大小的内存。 这个 API 使用到了第一步中创建的 VkBuffer。
- 确定内存类型:与图像资源类似,从可用的内存类型中获取合适的内存类型,并选择与用户所需属性匹配的内存类型。
- 分配设备内存:使用 vkAllocateMemory()API 分配设备内存(VkDeviceMemory)。
- 暂存:分配物理内存后,需要使用 vkMapMemory()将其映射到本地主机,以便将几何图形的数据从主机内存上载到物理设备内存。 一旦数据被复制到物理设备内存中,就需要使用 vkUnmapMemory()来取消映射。
- 绑定分配的内存:使用 vkBindBufferMemory()API 将设备内存(VkDeviceMemory)绑定到缓冲区对象(VkBuffer)。
下图总结了完整的缓冲区资源创建的工作流程:
实现缓冲区资源 - 为几何图形创建顶点缓冲区
VulkanDrawable 类包含 createVertexBuffer()函数,该函数通过缓冲区资源的帮助将几何数据存储在 GPU 内存中。 该实现与创建图像资源非常相似,但有一点区别。 在这个实现中,不需要创建缓冲区视图;相反,会直接使用缓冲区对象。
一些 Vulkan 实现可以提供获取和格式化顶点属性的能力,将来自缓冲区的顶点输入数据转换为专用的固定功能硬件,而不是把执行提取操作作为顶点着色器的一部分。 一旦实现了缓冲区资源,其绑定点就会存储在(VkVertexInputBindingDescription)控制结构中。 这个结构描述了用来获取顶点的若干缓冲区。
类似地,属性存储在 VkVertexInputAttributeDescription 结构中;它描述了每个属性的格式和布局,以将它们从顶点缓冲区读取到着色器变量。 这两种信息都会在创建管线时使用,用来指定顶点的输入状态。
以下是 VkVertexInputBindingDescription 的语法信息:
typedef struct VkVertexInputBindingDescription {
uint32_t binding;
uint32_t stride;
VkVertexInputRate inputRate;
} VkVertexInputBindingDescription;
我们来看看这个结构的各个字段:
binding | 该参数表示一个无符号 32 位整数的绑定数字,用于描述控制结构。
stride | 这是以字节为单位指定的;该参数指示缓冲区内连续元素之间的偏移量。
inputRate | 该参数指示是否在每个顶点或实例基础上指定了缓冲区属性。 该参数采用以下枚举:
typedef enum VkVertexInputRate {
VK_VERTEX_INPUT_RATE_VERTEX = 0,
VK_VERTEX_INPUT_RATE_INSTANCE = 1, } VkVertexInputRate;
VK_VERTEX_INPUT_RATE_VERTEX 表示将按照顶点索引基准消耗顶点属性。VK_VERTEX_INPUT_RATE_INSTANCE 表示将根据实例索引基础消耗顶点属性。
同样,这里是 VkVertexInputAttributeDescription 结构的语法信息:
typedef struct VkVertexInputAttributeDescription {
uint32_t location;
uint32_t binding;
VkFormat format;
uint32_t offset;
} VkVertexInputAttributeDescription;
VkVertexInputAttributeDescription 中的各个字段如下:
location :此字段指示该属性的着色器绑定位置。
binding :这是属性消耗数据的绑定编号(从哪个数据开始消耗)。
format :顶点属性的数据大小和类型由该参数指示。
offset :该参数表示该属性从顶点输入绑定中元素起始的字节偏移量。
顶点缓冲区是在 createVertexBuffer()函数中创建的,如以下代码片段所示。 该函数接受包含顶点数据、其大小和步幅信息(如果有)的主机内存信息作为输入参数。 最后一个参数是一个布尔标志,用于表明几何数据是否包含纹理坐标:
void VulkanDrawable::createVertexBuffer(const void vertexData, uint32_t dataSize, uint32_t dataStride, bool useTexture)
{
VulkanApplication appObj = VulkanApplication::GetInstance(); VulkanDevice* deviceObj = appObj->deviceObj;
VkResult result; bool pass;
// Create the Buffer resource metadata information
VkBufferCreateInfo bufInfo = {};
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufInfo.pNext = NULL;
bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; bufInfo.size = dataSize; bufInfo.queueFamilyIndexCount = 0; bufInfo.pQueueFamilyIndices = NULL;
bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
bufInfo.flags = 0;
// Create the Buffer resource
result = vkCreateBuffer(deviceObj->device, &bufInfo,
NULL, &VertexBuffer.buf);
// Get the Buffer resource requirements VkMemoryRequirements memRqrmnt; vkGetBufferMemoryRequirements(deviceObj->device,
VertexBuffer.buf, &memRqrmnt);
// Create memory allocation metadata information
VkMemoryAllocateInfo alloc_info = {};
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; alloc_info.pNext = NULL; alloc_info.memoryTypeIndex = 0; alloc_info.allocationSize = memRqrmnt.size;
// Get the compatible type of memory
pass = deviceObj->memoryTypeFromProperties (memRqrmnt.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
&alloc_info.memoryTypeIndex);
// Allocate the physical backing for buffer resource
result = vkAllocateMemory(deviceObj->device,
&alloc_info, NULL, &(VertexBuffer.mem));
VertexBuffer.bufferInfo.range = memRqrmnt.size; VertexBuffer.bufferInfo.offset = 0;
// Map the physical device memory region to the host
uint8_t *pData;
result = vkMapMemory(deviceObj->device, VertexBuffer.mem,
0, memRqrmnt.size, 0, (void **)&pData);
// Copy the data in the mapped memory
memcpy(pData, vertexData, dataSize);
// Unmap the device memory
vkUnmapMemory(deviceObj->device, VertexBuffer.mem);
// Bind the allocated buffer resourece to the device memory
result = vkBindBufferMemory(deviceObj->device,
VertexBuffer.buf, VertexBuffer.mem, 0);
// The VkVertexInputBinding viIpBind, stores the rate at
// which the information will be injected for vertex input
viIpBind.binding = 0;
viIpBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; viIpBind.stride = dataStride;
// The VkVertexInputAttribute - Description) structure, store
// the information that helps in interpreting the data.
viIpAttrb[0].binding = 0;
viIpAttrb[0].location = 0;
viIpAttrb[0].format = VK_FORMAT_R32G32B32A32_SFLOAT; viIpAttrb[0].offset = 0;
viIpAttrb[1].binding = 0;
viIpAttrb[1].location = 1; viIpAttrb[1].format = useTexture ?
VK_FORMAT_R32G32_SFLOAT : VK_FORMAT_R32G32B32A32_SFLOAT;
viIpAttrib[1].offset = 16;
}
理解代码的流程
让我们详细看一下前面的实现。 首先,创建 VkCreateBufferInfo 结构并使用顶点缓冲区元数据进行填充。 这是我们存储缓冲区用法 usage 类型的地方;此用法 usage 类型引用顶点信息(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)。 其他的用法类型可以是索引缓冲区、Uniformuniform 缓冲区、纹理缓冲区等等。 以字节为单位指定顶点缓冲区数据的大小。 由于没有涉及多个队列,因此我们可以将共享模式标记为 VK_SHARING_MODE_EXCLUSIVE。 将此结构传递给 vkCreateBuffer()API 以创建顶点缓冲对象(VertexBuffer :: buf)。
使用创建的缓冲区对象(VertexBuffer :: buf)传递给 vkGetBufferMemoryRequirements()API,以便收集内存需求(memRqrmnt)用来在 API 中分配缓冲区。 在缓冲区资源分配过程中,此信息有助于分配所需的适当尺寸的内存。 该 API 使用到了 VkCreateBufferInfo 控制结构。
接下来,为分配过程做好准备,并根据收集的内存需求创建 VkMemoryAllocateInfo(allocInfo)。 以字节为单位指定要分配的尺寸信息并获取用于分配的兼容内存类型。 使用 vkAllocateMemory 分配内存,传入 allocInfo,并在 VertexBuffer.mem 中检索 VkDeviceMemory 类型的设备内存。
分配物理内存后,使用 vkMapMemory()映射本地的主机内存,以便能够将几何数据复制到物理设备内存。 将数据复制到物理设备内存后,需要使用 vkUnmapMemory()取消映射。
使用 vkBindBufferMemory()API 并将设备内存(VkDeviceMemory)绑定到缓冲对象(VkBuffer)。
注意
这个例子不需要创建缓冲区对象视图。 缓冲区视图(VkBufferView)只有在缓冲区数据要被着色器使用的时候才会创建。 使用 vkCreateBufferView()API 创建缓冲区视图。
以下是上述过程的图示。 它显示了三列,其中第一列显示了预期的作业,第二列定义了用于完成作业的 Vulkan API,第三列指定了返回的值类型。
作为最后一步,我们需要将创建的缓冲区资源绑定到底层管线,以便可以在 viIpBind(类型为 VkVertexInputBindingDescription)中定义顶点输入信息的速率 - 输入速率可以基于顶点或实例。
VkVertexInputAttributeDescription 结构对象的 viIpAttrb 存储与位置和颜色属性相关的信息,并用于解释数据。 例如,在 viIpAttrb 中,我们指定绑定点,绑定中的位置,预期的数据格式和偏移量信息。 在非交错数据的情况下,它应该是 0. 由于我们的数据是交错形式的,因此对于位置和颜色属性分别是 0 和 16; 如下图所示。 useTexture 将顶点数据的一部分解释为纹理坐标或颜色。 如果指定为 true,则表示顶点数据包含纹理坐标(两个分量,其中每个分量均为 32 位),而不是颜色分量(四个分量,每个分量均为 32 位的)。
理解渲染通道
渲染通道告诉我们关于渲染时要使用的帧缓冲附件和子通道的信息,如颜色和深度附件,指示了会在其中显示多少颜色图像和深度图。 它(附件?渲染通道?)规定了用来表示它们每一个的样本位以及如何在渲染过程中使用其内容。 它(附件?)还确认在每个 Render Pass 实例的开始和结束时如何处理其中的内容。 在命令缓冲区中使用的渲染通道称为渲染通道实例,管理子通道之间的依赖关系,并定义关于如何在子通道上使用附件的协议。
渲染通道主要由两种类型的组件组成:附件和子通道。 以下是关于附件和子通道的一些事实。
附件 Attachments
附件是指渲染命令时使用的表面区域(例如颜色,深度 / 模板或用来执行解析操作的解析附件)。 这里描述了五种类型的附件:
- 颜色附件:Color attachment,颜色附件表示在其上绘制渲染图元的、用来绘图的目标图像。
- 深度附件:Depth attachment,深度附件存储深度信息并将其用于深度 / 模板测试操作。
- 解析附件:Resolve attachment, 解析附件自动从多采样附件一直向下采样到子通道末尾相应的单采样附件。 解析附件对应于多重采样的颜色附件,并且表现得好像子通道末尾有一个 vkCmdResolveImage,从彩色附件到相应的解析附件。 一个例外是驱动程序可能会做得更好,例如同时对一个贴片 tiler 执行溢出 spill 和解析 resolve 操作。
- 输入附件:Input attachment,这个附件由着色器共享的附件列表组成。 输入附件类似于受限纹理,其中着色器可以执行的唯一操作是纹素的提取(texture(tex,uv)) - 即从纹素读取(对应于当前正被着色的像素)。 明显的例子是一触式后(期)处理过滤器(无模糊等),经典的延迟渲染器的光照阶段就是从 G 缓冲区中读取等。
- 保留附件:Preserve attachment,在整个过程中,在给定的子通道中,保留附件内的内容保持不变。 保留附件根本不在其他 API 中表达。 他们表示要求保留一些附件的内容(因为它将在以后使用),但不应该被当前的子通道所触及。 这在桌面 GPU 上根本没有意义,其中渲染目标写入操作会直接进入内存。 然而,对于平铺器 tiler 来说,这就显得非常有趣:在子通道期间,片上内存的附件部分可以被重新使用以代替某些其他附件,而不必将其内容泄漏回内存。
在 Vulkan API 中,VkAttachmentDescription 描述符可用于指定附件的各种属性。 这包括它的格式,样本数量,初始布局信息以及在每个 Render Pass 实例开始和结束时,它的内容的处理方式。
子通道 Subpasses
在渲染通道中,子通道读取和写入相关的附件。 Render Pass 执行中的当前子通道会受渲染命令的影响:
- 一个子通道可以读取先前被写入的附件(它必须保存)并写入当前与其关联的附件。
- 写入颜色和深度 / 模板缓冲区也是与 Render Pass 实例关联的子通道附件的一部分。
- 为了允许后续通道使用子通道附件,应用程序有责任确保信息保持有效状态,直到信息不被使用为止。
- 在整个子通道的生命周期中,还有保留附件来保存附件中的内容。 子通道不能影响这些附件,因为它们是读 / 写保护的。 换句话说,它们在子通道生命周期中不能被读 / 写。
子通道描述符由 VkSubpassDescription 定义。 它描述了子通道中涉及的附件数量。
注意
通过子通道上的高级信息,了解渲染通道中的子通道集,渲染通道为底层实现提供了一个优化子通道之间附件数据存储和传输的机会。 这意味着可以将多个通道合并在一起并使用一个 Render Pass 实例进行解析。
在我们的示例中,我们只处理一个子通道;因此,任何有关它的依赖关系的主题都不在本书的涵盖范围之内。
用于渲染通道的 Vulkan API
在本节中,我们将了解用于在 Vulkan 中实现渲染通道的各种 API。
Render Pass 对象是使用 vkCreateRenderPass()API 创建的。 该 API 接受定义附件的 VkCreateRenderPassInfo 控制结构,例如我们的交换链彩色图像和深度图。 它还包含一个重要的结构(VkRenderPassCreateInfo),定义了关于如何在单个 Render Pass 中处理这些附件的协议。
我们来看看 Render Pass API 规范,以便创建 Render Pass 对象:
VkResult vkCreateRenderPass(
VkDevice device,
const VkRenderPassCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkRenderPass* pRenderPass);
下表描述了此结构的各个参数:
device :这是用于 Render Pass 创建的逻辑设备句柄。
pCreateInfo :这是一个指向 VkRenderPassCreateInfo 结构对象的指针。
pAllocator : 这个字段控制着主机内存的释放过程。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pRenderPass :这个参数会创建 VkRenderPass 对象指针。
VkRenderPassCreateInfo 结构将附件和子通道与 Render Pass 对象相关联。 以下是其语法以及字段描述:
typedef struct VkRenderPassCreateInfo {
VkStructureType type;
const void* pNext;
VkRenderPassCreateFlags flags;
uint32_t attachmentCount;
const VkAttachmentDescription* pAttachments;
uint32_t subpassCount;
const VkSubpassDescription* pSubpasses;
uint32_t dependencyCount;
const VkSubpassDependency* pDependencies;
} VkRenderPassCreateInfo;
下表介绍了此结构的字段:
type :这是这种结构的类型,它必须是 VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO。
next :这个字段或是 NULL,或是一个指向扩展特定的结构的指针。
flags :这是保留供将来使用。
attachmentCount :这指定了此 Render Pass 实例使用的附件数。 如果为零,则表示此 Render Pass 中没有附件。
attachments :这是一个指定 Render Pass 附件属性的 VkAttachmentDescription 结构数组, 数组大小等于 attachmentCount。
subpassCount : 这指定了此渲染通道中的子通道数量。
subpasses | 这是描述子通道属性的 VkSubpassDescription 结构数组。
dependencyCount :这指定了这些子通道对之间的依赖数量。 如果为零,则意味着不存在依赖。
dependencies :这是一个等于 VkSubpassDependency 结构的 dependencyCount 数的数组,描述了这些成对的子通道之间的依赖关系。 如果 dependencyCount 为零,则它必须为 NULL。
为了描述 Render Pass 中使用的每个附件,Vulkan 提供了 VkAttachmentDescription 控制结构。 以下是它的语法:
typedef struct VkAttachmentDescription {
VkAttachmentDescriptionFlags flags;
VkFormat format;
VkSampleCountFlagBits samples;
VkAttachmentLoadOp loadOp;
VkAttachmentStoreOp storeOp;
VkAttachmentLoadOp stencilLoadOp;
VkAttachmentStoreOp stencilStoreOp;
VkImageLayout initialLayout;
VkImageLayout finalLayout;
} VkAttachmentDescription;
这个表格描述了这个结构的各个字段:
flags :这是 VkAttachmentDescriptionFlags 类型的按位附加标志。
format :这是附件中使用的图像格式。
sample :这是指在 Render Pass 中用于附件的样本数量。
loadOp :这定义了颜色和深度附件的行为,以及在子通道开始时如何处理它们。 有关更多信息,请参阅以下部分中的 VkAttachmentStoreOp:
typedef enum VkAttachmentLoadOp {
VK_ATTACHMENT_LOAD_OP_LOAD = 0,
VK_ATTACHMENT_LOAD_OP_CLEAR = 1,
VK_ATTACHMENT_LOAD_OP_DONT_CARE = 2, } VkAttachmentLoadOp;
这些标志定义如下:VK_ATTACHMENT_LOAD_OP_LOAD:使用此标志可保留附件的现有内容;这意味着渲染区域的内容在每次渲染通道执行时都会保留。 VK_ATTACHMENT_LOAD_OP_CLEAR:渲染区域的内容用渲染通道开始定义的指定常量颜色值清除。 每次执行 Render Pass 后,首先用指定的颜色清除背景,然后在其上绘制图元。 VK_ATTACHMENT_LOAD_OP_DONT_CARE:渲染区域内的内容未定义,并且未保留。 这表示在 Render Pass 实例完成后,应用程序不需要缓冲区的内容。
storeOp :这定义了颜色和深度附件的行为,以及在子通道末尾如何处理它们。 有关更多信息,请参阅以下部分中的 VkAttachmentStoreOp。
typedef enum VkAttachmentStoreOp {
VK_ATTACHMENT_STORE_OP_STORE = 0,
VK_ATTACHMENT_STORE_OP_DONT_CARE = 1, } VkAttachmentStoreOp;
VK_ATTACHMENT_STORE_OP_STORE 表示渲染区域内的内容会被写入内存,并且在渲染通道实例完成后即可用于读取,也就是说,写入操作已经被同步了,使用标记 VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT(用于彩色附件)或 VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT(用于深度 / 模板附件)。换句话说,应用程序想要将渲染结果保留在这个内存中,以便它可以在随后的子通道中使用或打算将其呈现给显示器。VK_ATTACHMENT_STORE_OP_DONT_CARE 表示渲染完成后渲染区域内的内容就不再需要,可能会被丢弃;该附件的内容将在渲染区域内未定义。
stencilLoadOp :这使用 VkAttachmentLoadOp 定义了深度 / 模板附件的模板方面的行为,在子通道开始时将如何处理它。
stencilStoreOp :这使用 VkAttachmentStoreOp 定义了深度 / 模板附件的模板方面的行为,在子通道开始时将如何处理它。
initialLayout : 这定义了 Render Pass 实例开始时子资源所处的图像附件的布局。
finalLayout : 这定义了当 Render Pass 实例结束时子资源将被转换到的图像附件的布局。 在给定的渲染通道实例中,如果需要,附件可以在每个子通道中使用不同的布局。
子通道可以引用各种附件,如输入,解析,颜色,深度 / 模板以及保留附件。 这是使用一个称为 VkSubpassDescription 的特殊控制结构来描述的。下面是这个结构的语法:
typedef struct VkSubpassDescription {
VkSubpassDescriptionFlags flags;
VkPipelineBindPoint pipelineBindPoint;
uint32_t inputAttachmentCount;
const VkAttachmentReference* pInputAttachments;
uint32_t colorAttachmentCount;
const VkAttachmentReference* pColorAttachments;
const VkAttachmentReference* pResolveAttachments;
const VkAttachmentReference* pDepthStencilAttachment;
uint32_t preserveAttachmentCount;
const uint32_t* pPreserveAttachments;
} VkSubpassDescription;
这个结构的各个字段的描述如下所示:
flags :该字段未被使用;保留供将来使用。
pipelineBindPoint :这指定了子通道是属于图形队列还是计算队列。 它接受一个 VkPipelineBindPoint 值。
inputAttachmentCount :这指定了输入附件的数量。
pInputAttachments :这是一个大小等于 inputAttachmentCount 的 VkAttachmentReference 结构数组。 它指定了哪些附件可以在着色阶段读取,以及子通道期间图像附件的布局是什么。
colorAttachmentCount : 这是指颜色附件的数量。
pColorAttachments :这是一个大小等于 colorAttachmentCount 的 VkAttachmentReference 结构数组,其中包含 Render Pass 的附件,该附件将用作子通道中的颜色附件。 此外,它还指定了子通道期间附件图像所处的布局。
pResolveAttachments :这是一个指向 VkAttachmentReference 结构数组的指针。 它的每个元素(在 colorAttachments 中具有相同的索引)对应于一个颜色附件。 pDepthStencilAttachment :这指定了哪个附件将用于深度 / 模板数据以及它在子通道中所处的布局;它是一个指向 VkAttachmentReference 的指针。
preserveAttachmentCount :这是指保留附件的数量。
pPreserveAttachments :这是 preserveAttachmentCount 个 Render Pass 附件索引的一个数组,用于描述未被子通道使用的附件;这些索引反而描述了在子通道中必须保留哪些内容。
实现渲染通道 Render Pass
让我们一步一步地在现有的 VulkanRenderer 类中实现 Render Pass:
- 在 VulkanRenderer.h 中的 VulkanRender 类中包含以下 Render Pass 方法和变量,其中包括创建 / 销毁渲染通道以及与渲染通道关联的命令缓冲区:
class VulkanRenderer
{
// Many lines skipped, please refer to the source code
public:
/***** Member functions *****/
// Record render pass command buffer
void createRenderPassCB(bool includeDepth);
// Render Pass creation
void createRenderPass(bool includeDepth, bool clear=true);
// Destroy the render pass object when no more required
void destroyRenderpass();
/***** Member variables *****/
// Render pass created object
VkRenderPass renderPass;
}
- 在初始化阶段,使用 VulkanRenderer :: createRenderPass()创建 Render Pass:
void VulkanRenderer::initialize()
{
const bool includeDepth = true; createRenderPass (includeDepth);
}
- 创建的颜色图像和深度图像需要在附件中指定。 为这两个图像类型创建大小等于 2 的 VkAttachmentDescription 类型的数组。 此结构中指定的信息将决定在渲染通道 Render Pass 的开始和结束时如何处理图像。 其中包含图像格式,样本数量,加载和存储操作等。
- 对于这两个附件,将 loadOp 成员设置为 VK_ATTACHMENT_LOAD_OP_CLEAR。 这个设置的意思是在每个 Render Pass 实例开始时清除缓冲区。 对于颜色附件,将 storeOp 成员设置为 VK_ATTACHMENT_STORE_OP_STORE,指示渲染的输出保存在以后用于显示它的缓冲区中。
- 接下来,对于这两个附件,在 Render Pass 中指定绑定点。 这可以让渲染通道知道在哪里寻找一个特定的附件。 绑定点在 VkAttachmentReference 的帮助下定义;该控制结构摄入的另一条信息就是用于图像布局转换的图像布局信息。 分别为颜色和深度缓冲区使用 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 和 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL 指定图像布局。 该布局通知底层引擎将图像存储在最佳内存中以提供最佳性能。
- 使用 VkSubpassDescription 在子通道中指定所有附件。 在这里,指定了颜色,深度,解析以及保存附件。 最后,所有附件和子通道特定的信息都在 VkRenderPassCreateInfo 中汇聚并传递给 vkCreateRenderPass()API 以创建 Render Pass 对象:
void VulkanRenderer::createRenderPass(bool isDepthSupported,
bool clear)
{
// Dependency on VulkanSwapChain::createSwapChain() to
// get the color image and VulkanRenderer::
// createDepthImage() to get the depth image.
VkResult result;
// Attach the color buffer and depth buffer as an
// attachment to render pass instance VkAttachmentDescription attachments[2]; attachments[0].format = swapChainObj->scPublicVars.format; attachments[0].samples= NUM_SAMPLES;
attachments[0].loadOp = clear ? VK_ATTACHMENT_LOAD_OP_CLEAR
: VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; attachments[0].stencilLoadOp =
VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp =
VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout =
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachments[0].finalLayout =
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachments[0].flags = 0;
// Is the depth buffer present the define attachment
// properties for depth buffer attachment.
if (isDepthSupported)
{
attachments[1].format = Depth.format; attachments[1].samples = NUM_SAMPLES;
attachments[1].loadOp = clear ?
VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].storeOp =
VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].stencilLoadOp =
VK_ATTACHMENT_LOAD_OP_LOAD;
attachments[1].stencilStoreOp=
VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
attachments[1].flags = 0;
}
// Define the color buffer attachment binding point
// and layout information VkAttachmentReference colorReference = {}; colorReference.attachment = 0; colorReference.layout =
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
// Define the depth buffer attachment binding point and
// layout information
VkAttachmentReference depthReference = {}; depthReference.attachment = 1; depthReference.layout =
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
// Specify the attachments - color, depth,
// resolve, preserve etc. VkSubpassDescription subpass = {}; subpass.pipelineBindPoint =
VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.flags = 0;
subpass.inputAttachmentCount = 0; subpass.pInputAttachments = NULL; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorReference; subpass.pResolveAttachments = NULL; subpass.pDepthStencilAttachment = isDepthSupported ?
&depthReference : NULL; subpass.preserveAttachmentCount = 0; subpass.pPreserveAttachments = NULL;
// Specify the attachement and subpass associate with
// render pass
VkRenderPassCreateInfo rpInfo = {};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpInfo.pNext = NULL; rpInfo.attachmentCount = isDepthSupported ? 2 : 1; rpInfo.pAttachments = attachments;
rpInfo.subpassCount = 1;
rpInfo.pSubpasses = &subpass;
rpInfo.dependencyCount = 0; rpInfo.pDependencies = NULL;
// Create the render pass object
result = vkCreateRenderPass(deviceObj->device, &rpInfo,
NULL, &renderPass);
assert(result == VK_SUCCESS);
}
- 在销毁阶段,使用 vkDestroyRenderPass()API 销毁 Render Pass 对象:
void VulkanApplication::deInitialize()
{
rendererObj->destroyRenderpass();
. . . .
}
void VulkanRenderer::destroyRenderpass()
{
vkDestroyRenderPass(deviceObj->device, renderPass, NULL);
}
使用 Render Pass 以及创建 framebuffer
一旦创建 Render Pass,它就被用来创建帧缓冲区 framebuffer。 理想情况下,对于每个交换链彩色图像,我们需要一个与其相关的帧缓冲区。 例如,如果我们有一个双缓冲区交换链图像,那么我们需要两个帧缓冲区:一个用于前端缓冲区图像,另一个用于后端缓冲区图像。
Vulkan 中的帧缓冲区是使用 vkCreateFrameBuffer()API 创建的。 与其他 Vulkan API 一样,它也有一个名为 VkFrameBufferCreateInfo 的创建信息控制结构。 有关其语法和用法的更多信息,请参阅以下内容:
VkResult vkCreateFrameBuffer(
VkDevice device,
const VkFramebufferCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkFramebuffer* pFrameBuffer);
下表介绍了 vkCreateFrameBuffer()API 的各个参数:
device :这是帧缓冲区关联的逻辑设备句柄。
pCreateInfo :这是指向 VkFrameBufferCreateInfo 结构对象的指针。
pAllocator:这控制了主机内存的释放过程。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pFrameBuffer :这会创建 VkFrameBuffer 对象并返回其指针。
下面的语法描述了 VkFrameBufferCreateInfo 控制结构:
typedef struct VkFramebufferCreateInfo {
VkStructureType type;
const void* pNext;
VkFramebufferCreateFlags flags;
VkRenderPass renderPass;
uint32_t attachmentCount;
const VkImageView* pAttachments;
uint32_t width;
uint32_t height;
uint32_t layers;
} VkFramebufferCreateInfo;
下表描述了此结构的各个字段:
type : 这指的是这个结构的类型,它必须是 VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO。
next :这个字段或是 NULL 或指向扩展特定的结构的指针。
flag : 这必须是 0; 保留供将来使用。
renderPass :这是我们在前一节创建的 VkRenderPass 对象。
attachmentCount:这是指与帧缓冲区关联的附件数量。
attachment :这是对应图像的 VkImageView 句柄的一个数组,其中的每一个都将用作 Render Pass 实例中的相应附件。
width :这是指以像素为单位的帧缓冲区 framebuffer 的宽度。
height :这是指以像素为单位的帧缓冲区的高度。
layers:这是指帧缓冲区中的层。
实现帧缓冲区 framebuffer
帧缓冲区的实现很简单;按着如下步骤操作即可:
- 在 VulkanRenderer 类中创建以下函数和变量。 cmdFrameBuffer 声明负责创建帧缓冲区(frameBuffer)的命令缓冲区:
class VulkanRenderer
{
public:
// Member functions
void createFrameBuffer(bool includeDepth,bool clear= true); void destroyFrameBuffer ();
// Number of frame buffer corresponding to each swap chain
std::vector framebuffers;
}
- 初始化期间,使用 createFrameBuffer()函数创建帧缓冲区:
void VulkanRenderer::initialize()
{
const bool includeDepth = true; createFrameBuffer(includeDepth);
}
- 对于每个交换链彩色图像,创建其相应的帧缓冲区。 这是通过使用 vkCreateFrameBuffer()API 完成的。 此 API 使用 VkFrameBufferCreateInfo,我们在其中指定深度和彩色图像视图作为附件。 此外,在此结构中还传递了创建的 Render Pass 对象以及 framebuffer 的尺寸:
void VulkanRenderer::createFrameBuffer(bool includeDepth)
{
// Dependency on createDepthBuffer(), createRenderPass()
// and recordSwapChain() VkResult result; VkImageView attachments[2];
attachments[1] = Depth.view;
VkFramebufferCreateInfo fbInfo = {};
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbInfo.pNext = NULL; fbInfo.renderPass = renderPass; fbInfo.attachmentCount = includeDepth ? 2 : 1; fbInfo.pAttachments = attachments; fbInfo.width = width; fbInfo.height = height;
fbInfo.layers = 1;
uint32_t i; framebuffers.clear();
framebuffers.resize(swapChainObj->scPublicVars.
swapchainImageCount);
for (i = 0; i < swapChainObj->scPublicVars
.swapchainImageCount; i++) { attachments[0] = swapChainObj->scPublicVars.
colorBuffer[i].view;
result = vkCreateFramebuffer(deviceObj->device, &fbInfo, NULL, &framebuffers.at(i));
assert(result == VK_SUCCESS);
}
}
- 在应用程序的销毁阶段,使用 vkDestroyFrameBuffer()API 销毁所有的帧缓冲区:
void VulkanApplication::deInitialize()
{
rendererObj->destroyRenderpass();
. . . .
}
void VulkanRenderer::destroyFramebuffers()
{
for (uint32_t i = 0; i < swapChainObj
->scPublicVars.swapchainImageCount; i++) { vkDestroyFramebuffer(deviceObj->device,
framebuffers.at(i), NULL);
}
framebuffers.clear();
}
创建背景颜色
在本节中,我们使用创建的 Render Pass 和 framebuffer 对象并实现 Render Pass 实例。 这个 Render Pass 实例非常简单,只会用指定的颜色清除背景图像。 对于每个交换链图像,可以使用 VkRenderPassBeginInfo 结构的 pClearValues 字段指定不同的颜色;这个结构然后被传递给 Render Pass 实例。
Render Pass 实例是在创建命令缓冲区的准备阶段实现的。 对于每个交换链图像,都会创建一个相应的命令缓冲区对象。 这意味着,对于 n 个交换链图像,我们需要创建 n 个命令缓冲区对象。
准备工作使用 VulkanDrawable :: prepare()函数完成,交换链图像的渲染会使用 VulkanDrawable :: render()函数完成。
下图显示了 prepare()和 render()函数的调用栈:
以下类声明显示了添加到 VulkanDrawable 类中的新成员变量和函数。 prepare()函数在 vecCmdDraw vector 中生成命令缓冲区并记录绘图命令;它们会用于 render()函数中,其中会执行命令缓冲区并渲染交换链图像。 recordCommandBuffer()函数将这些命令记录在 Render Pass 实例。
注意
有关渲染绘制对象的前期准备工作的详细信息,请参阅第 9 章“绘制对象”中的“准备绘图对象和绘制绘图对象”部分。
class VulkanDrawable
{
public:
// Prepares the drawing object before rendering
// Allocate, create, record command buffer
void prepare();
// Renders the drawing object
void render();
private:
// Command buffer for drawing
std::vector vecCmdDraw;
// Prepares render pass instance
void recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw);
};
在 Render Pass 实例中设置背景色
在本节中,我们将实现 VulkanDrawable 的 prepare()函数。 在这个函数里面,命令缓冲区包装类(CommandBufferMgr)用于管理(分配,记录以及提交)命令缓冲区(vecCmdDraw)。 以下代码实现了 prepare()函数:
void VulkanDrawable::prepare()
{
VulkanDevice* deviceObj = rendererObj->getDevice(); vecCmdDraw.resize(rendererObj->getSwapChain()->scPublicVars
.colorBuffer.size());
// For each swapbuffer color image buffer
// allocate the corresponding command buffer
for (int i = 0; i < rendererObj->getSwapChain()->scPublicVars.
colorBuffer.size(); i++){
// Allocate, create and start command buffer recording
CommandBufferMgr::allocCommandBuffer(&deviceObj->device,
*rendererObj->getCommandPool(), &vecCmdDraw[i]); CommandBufferMgr::beginCommandBuffer(vecCmdDraw[i]);
// Create the render pass instance
recordCommandBuffer(i, &vecCmdDraw[i]);
// Finish the command buffer recording
CommandBufferMgr::endCommandBuffer(vecCmdDraw[i]);
}
}
该实现首先检查交换链支持的彩色图像的数量,并创建相同数量的命令缓冲区对象,将它们中的每一个逻辑地与相应的交换链图像相关联。 交换链图像的清除总是在后端图像(后缓冲区)上执行,而前端图像(前缓冲区)用于显示渲染内容。 一旦后端的图像被清除并被渲染(如果有的话),它就会与前端缓冲区交换。
使用创建的命令缓冲区(vecCmdDraw)并在 recordCommandBuffer()函数内记录这些命令。 此函数使用传递到 vkCmdBeginRenderPass()API 的 VkRenderPassBeginInfo 数据结构的 pClearValues 字段为每个交换链图像指定清除颜色值。 vkCmdBeginRenderPass()和 vkCmdEndRenderPass()API 定义了记录 Render Pass 实例命令的范围。
注意
有关 Render Pass 命令及其相关 API 的更多信息,请参阅第 9 章“绘图对象”中的“记录渲染通道 Render Pass 命令”部分。
有两个清除值对象(VkClearValue)。 第一个指定关联的交换链图像(由 currentImage 索引指示)的清除颜色值。 第二个对象指定要用于深度图的清除颜色值:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw)
{
// Specify the clear color value VkClearValue clearValues[2]; switch (currentImage)
{
case 0: clearValues[0].color = { 1.0f,0.0f,0.0f,1.0f };break; case 1: clearValues[0].color = { 0.0f,1.0f,0.0f,1.0f };break; case 2: clearValues[0].color = { 0.0f,0.0f,1.0f,1.0f };break; default:clearValues[0].color = { 0.0f,0.0f,0.0f,1.0f };break;
}
// 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.renderPass = rendererObj->renderPass; renderPassBegin.framebuffer = rendererObj-> framebuffers[currentImage]; 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);
// End of render pass instance recording
vkCmdEndRenderPass(cmdDraw);
}
渲染上色后的背景
清除颜色的交换链图像在 render()函数内逐个渲染,如以下代码片段中所示。 WSI 窗口系统扩展 vkAcquireNextImageKHR()用于查询下一个可用的交换链图像索引。 该索引指示哪个交换链图像可用于绘图。 使用这个索引,选择相应的命令缓冲区并提交给队列。 一旦在 GPU 上处理完成,交换链图像即可使用展示引擎进行显示。 这种展示操作使用 WSI 扩展 fpQueuePresentKHR 来执行。 该 API 使用 VkPresentInfoKHR 结构;这个结构包含交换链对象和需要在窗口上显示的交换链图像的索引。
注意
渲染绘图对象是一个独立的主题,超出了本章的范围。 有关此主题和相关 WSI 扩展及其相关数据结构的更多信息,请参阅第 9 章“绘图对象”中的“绘制绘图对象”部分。
以下代码实现了清除操作 ------ 每秒用红色,蓝色和绿色背景色清除背景色:
void VulkanDrawable::render()
{
VulkanDevice deviceObj = rendererObj->getDevice(); VulkanSwapChain* swapChainObj = rendererObj->getSwapChain();
uint32_t& currentColorImage = swapChainObj->scPublicVars.
currentColorBuffer; VkSwapchainKHR& swapChain = swapChainObj->scPublicVars.
swapChain;
// Render each background color for 1 second.
Sleep(1000);
// Get the index of the next available swapchain image: VkResult result = swapChainObj->fpAcquireNextImageKHR (deviceObj->device, swapChain, UINT64_MAX, VK_NULL_HANDLE, VK_NULL_HANDLE, ¤tColorImage);
// Queue the command buffer for execution CommandBufferMgr::submitCommandBuffer(deviceObj->queue, &vecCmdDraw[currentColorImage], NULL);
// Present the image in the window
VkPresentInfoKHR present = {};
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present.swapchainCount = 1;
present.pSwapchains = &swapChain;
present.pImageIndices = ¤tColorImage;
// Queue the image for presentation, result = swapChainObj->fpQueuePresentKHR (deviceObj->queue, &present); assert(result == VK_SUCCESS);
}
以下是实现的实际输出效果。 输出会显示各种背景颜色,每个图像将显示一秒钟。
Vulkan 中使用着色器
着色器是在图形管线和计算管线中控制可编程阶段的一种手段。
图形管线包括顶点,细分,几何和片段着色器。 前四个 ------ 顶点、细分控制、细分评估以及几何着色器共同负责顶点的处理阶段。 随后是片段着色器,它在光栅化之后执行。
这里有一些关于图形管线着色器的内容:
- 顶点着色器:Vertex shaders,图形管线执行顶点着色器会导致最初的数据装配, 它用于处理几何图形的顶点,处理后续着色器可能需要的数据(如果启用的话),例如片段着色器需要的照明信息
- 细分着色器:Tessellation shaders,这是一个顶点处理阶段。 启用后,会将顶点数据的补丁细分为更小的图元,由控制和评估着色器对其进行控制。
- 几何图形着色器:Geometry shaders,当启用时,该着色器能够在执行时通过使用以前着色器阶段的输出(细分着色器和顶点着色器)来生成新的几何图形。
- 片段着色器:Fragment shaders,光栅化器(固定功能)使用前面阶段处理过的顶点数据生成片段。 然后由片段着色器处理这些片段,该着色器负责对它们着色。
- 计算着色器:Compute shaders,在工作组中调用计算着色器,用于以大规模并行计算的格式计算任意信息。 计算管线只包含计算着色器。
与 GLSL 是官方着色语言的 OpenGL 不同,Vulkan 使用 SPIR-V,对于着色器和计算核心来说,这是一种全新的方法。
引入 SPIR-V
与OpenGL shading language (GLSL)不同,这是用于着色器的一种人类可读的形式,而 Vulkan 完全依赖于 SPIR-V,它是一种低级二进制中间表示intermediate representation(IR)。 由于 SPIR-V 并不需要使用高级语言,它显然降低了驱动程序的复杂性。 这允许应用程序接受任何高级语言(GLSL 或 HLSL),只要 GPU 供应商提供编译器将其转换为 SPIR-V 格式即可。
SPIR-V 是一种二进制中间语言,用于表示图形着色器阶段和多个 API 的计算核心。 它将信息存储在一个 32 位字的流中。 它主要由两部分组成:头部和有效载荷。 头部由前 5 个槽组成(5 x 4 字节 = 20 字节),有助于将输入源识别为 SPIR-V 输入流。 另外,有效负载表示包含源数据的可变长度指令。
如下图所示,SPIR-V 是独立于平台的中间语言,在底层,可以由驱动程序使用的若干上层语言所使用。 SPIR-V 的编译器存在多种源语言,例如 GLSL 或 HLSL,甚至用于读取计算内核。 着色器模块提供了多个入口点,可以在着色器代码大小和磁盘 I / O 要求方面提供良好的优势。
下图来自 Khronos 官方 SPIR-V 规范,提供了 SPIR-V 文件格式的描述:
尽管 SPIR-V 是一种高级语言,不过也比较简单,可以绕过所有文本或字符串的解析,这对于获得更高的性能是一件好事。 根据官方规范,SPIR-V 首先会编码注释和修饰符,然后是函数集合。 每个函数都会使用附加指令对基本块的控制流程图 control flow graph(CFG)进行编码,以保留源代码结构的控制流程。 加载、存储指令用来访问声明的变量,其中包括所有的 I / O。 绕过加载、存储的中间结果使用静态的单赋值 static single assignment(SSA)来表示。 数据对象用层次化的类型信息进行逻辑表示。 聚合体或物理寄存器储库的分配等并没有扁平化。 可选择的寻址模型用来确定是否可以使用通用指针,或者内存访问是否是纯逻辑的。
把 GLSL 着色器编译为 SPIR-V
在本节中,我们将实现一个非常简单的顶点和片段着色器,使用 GLSL 语言,并将其转换为 SPIR-V 格式,以便在我们的 Vulkan 程序中使用。 有两种方式可以将 GLSL 着色器转换为 SPIR-V 二进制形式 ,即离线和在线。 前者使用可执行文件将 GLSL 源代码转换为 SPIR-V 本地格式文件;然后把这个文件注入到正在运行的 Vulkan 程序中。 后者使用动态库将 GLSL 编译为 SPIR-V 格式。
使用 glslangValidator 可执行程序离线编译
预编译的 Lunar-G SDK 的二进制文件中包含 glslangValidator 可执行文件,可用于将 GLSL 转换为 SPIR-V 的.spv 格式。 这不需要在运行时编译数据,可以预先注入。 在这种方法中,开发人员不能更改 GLSL 程序,而且为了能够看到预先发生的效果,必须在每次添加更改后再次重新编译。 这种方式适用于不需要进行很多更改的产品发布版本。
在开发周期中,会希望频繁调整和调试,在线方法就是比较适合的。 有关这方面的更多信息,请参阅下一节。 另请参阅以下几点:
- 位置:在 Windows 系统上,glslangValidator 位于
- 用法:它的用法定义为 glslangValidator 『选项』 … 『文件』 …。
这里,每个文件都以.结尾,其中是以下内容之一:
阶段 Stage | 说明 —|--- .conf | configuration,提供一个可选的配置文件来替换默认的配置。 .vert | vertex shader ,用于顶点着色器。 .tesc | tessellation control shader,用于细分控制着色器。 .tese | tessellation evaluation shader,用于细分评估着色器。 .geom | geometry shader,用于几何图形着色器。 .frag | fragment shader,用于片段着色器。 .comp | compute shader,用于计算着色器。
- 示例:请参阅以下说明将 GLSL 源文件编译为 SPIR-V 格式(.spv):
- 打开终端,转到源文件夹(假设这个文件夹包含顶点着色器 Tri.vert),然后输入以下命令;这会产生输出 Tri-Vert.spv 的 SPIR-V 格式文件:
glslangValidator.exe - V Tri.vert - o Tri- Vert.spv
注意
glslangValidator.exe 可执行文件也可以使用 LunarG SDK glslang 的源代码进行构建。
使用 SPIR-V 工具库在线编译
Lunar SDK 还提供使用 GLslang 库的即时编译功能。 我们需要从 SDK 源代码中编译这些库,并将它们包含在我们的源项目中。 这些库暴露了一些特殊的 API,可用于将 GLSL 着色器源代码传递到项目中,使它以 SPIR-V 的格式对 Vulkan 着色器模块可用,具体的操作都是在幕后进行的。
为了构建源代码并将其编译到库中,您需要知道以下内容:
- 位置:使用
- CMake:GLslang 文件夹包含 CMakelists.txt,可用于构建特定于平台的项目。 在您构建 CMake 之后,就会创建以下项目:glslang,glslangValidator,OGLCompiler,OSDependent,SPIRV 和 spirv-remap。 在调试和发布模式下编译项目后,会在目标构建文件夹中生成必要的静态库。
- 所需的库:在 Windows 上需要以下库的支持才能将 GLSL 文件在线编译为 SPIR-V:
- SPIRV.lib
- glslang.lib
- OGLCompiler.lib
- OSDependent.lib
- HLSL.lib
实现着色器 Shader
现在是时候在我们的例子中引入一些着色器功能了。 本节将帮助我们逐步实现这一目标。 按照这里给出的指示来操作:
转到示例应用程序的 CMakeLists.txt 文件并进行以下更改:
- 项目名称:给项目命名并设置 Vulkan SDK 路径:
set (Recipe_Name “7e_ShadersWithSPIRV”)
- 头文件:包含头文件,位于 glslang 目录。 这将使我们能够包含源程序所需的 SPIRV / GlslangToSpv.h 头文件。
- 静态库:从 Vulkan-SDK 编译的项目中,我们需要用到 SPIRV,glslang,HLSL OGLCompiler 和 OSDependent 静态库。
- 链接库路径:提供解决方案用到的链接库路径,在该路径中解决方案可以找到前面指定的静态库。 变量名 VULKAN PATH 指定为 Lunar SDK 的路径。
- 以下 CMake 代码添加所需的库,用来将 GLSL 转换为 SPIR-V。您可以通过两种方式将 GLSL 代码转换为.spv 格式:a)离线:Offline,使用 Vulkan SDK 的 glslangValidator.exe,该工具不需要额外的库,Vulkan-1.lib 就已经足够了。当项目处于部署阶段时,此模式非常有用,其中着色器并不用再经历开发周期的动态更改 b)在线:Online,使用 glslang 辅助函数在运行时自动将 GLSL 转换为.spv。在项目开发阶段,开发人员不需要离线编译 GLSL 着色器进行着色器代码的调整。 glslang 辅助函数会自动将 GLSL 编译为.spv 格式。此模式会需要用到以下几个库:SPIRV,glslang,OGLCompiler,OSDependent,HLSL。使用 CMake 的 BUILD_SPV_ON_COMPILE_TIME 变量,您可以选择将 GLSL 着色器转换为.spv 格式所需的选项。有关更多信息,请在现有的 CMakeLists.txt 中添加以下代码,并按照内嵌注释进行操作:
# BUILD_SPV_ON_COMPILE_TIME - accepted value ON or OFF
ON - Reads the GLSL shader file and auto convert in SPIR- V form (.spv). This requires additional libraries support from VulkanSDK
like SPIRV glslang OGLCompiler OSDependent HLSL
OFF - Only reads .spv files, which need to be compiled offline using glslangValidator.exe.
For example: glslangValidator.exe - V - o
option(BUILD_SPV_ON_COMPILE_TIME “BUILD_SPV_ON_COMPILE_TIME” OFF)
if(BUILD_SPV_ON_COMPILE_TIME)
Preprocessor flag allows the solution to use glslang library functions
add_definitions(-DAUTO_COMPILE_GLSL_TO_SPV)
GLSL - use Vulkan SDK’s glslang library for compling GLSL to SPV # This does not require offline coversion of GLSL shader to
SPIR- V(.spv) form
set(GLSLANGDIR “ V U L K A N P A T H / g l s l a n g " ) g e t f i l e n a m e c o m p o n e n t ( G L S L A N G P R E F I X " {VULKAN_PATH}/glslang") get_filename_component(GLSLANG_PREFIX " VULKANPATH/glslang")getfilenamecomponent(GLSLANGPREFIX"{GLSLANGDIR}” ABSOLUTE)
Check if glslang directory exists
if(NOT EXISTS G L S L A N G P R E F I X ) m e s s a g e ( F A T A L E R R O R " N e c e s s a r y g l s l a n g c o m p o n e n t s d o n o t e x i s t : " {GLSLANG_PREFIX}) message(FATAL_ERROR "Necessary glslang components do not exist: " GLSLANGPREFIX)message(FATALERROR"Necessaryglslangcomponentsdonotexist:"{GLSLANG_PREFIX})
endif()
Include the glslang directory
include_directories( ${GLSLANG_PREFIX} )
If compiling GLSL to SPV using we need the following libraries
set(GLSLANG_LIBS SPIRV glslang OGLCompiler OSDependent HLSL)
Generate the list of files to link, per flavor.
foreach(x ${GLSLANG_LIBS})
list(APPEND VULKAN_LIB_LIST debug ${x}d optimized ${x})
endforeach()
Note: While configuring CMake for glslang we created the binaries in a “build” folder inside ${VULKAN_PATH}/glslang.
Therefore, you must edit the below lines for your custom path like /OGLCompilersDLL,
/OSDependent/Windows link_directories( V U L K A N P A T H / g l s l a n g / b u i l d / O G L C o m p i l e r s D L L ) l i n k d i r e c t o r i e s ( {VULKAN_PATH}/glslang/build/OGLCompilersDLL ) link_directories( VULKANPATH/glslang/build/OGLCompilersDLL)linkdirectories({VULKAN_PATH}/glslang/build/glslang/ OSDependent/Windows) link_directories( V U L K A N P A T H / g l s l a n g / b u i l d / g l s l a n g ) l i n k d i r e c t o r i e s ( {VULKAN_PATH}/glslang/build/glslang) link_directories( VULKANPATH/glslang/build/glslang)linkdirectories({VULKAN_PATH}/glslang/build/SPIRV) link_directories(${VULKAN_PATH}/glslang/build/hlsl)
endif()
在 VulkanShader.h / .cpp 中实现用户定义的着色器类 VulkanShader,用来管理着色器的所有活动。 请参阅此处的实现:
/************** VulkanShader.h **************/
#pragma once #include “Headers.h”
// Shader class managing the shader conversion, compilation, linking class VulkanShader
{
public:
VulkanShader() {} // Constructor
~VulkanShader() {} // Destructor
// Entry point to build the shaders
void buildShader(const char *vertShaderText, const char *fragShaderText);
// Convert GLSL shader to SPIR- V shader
bool GLSLtoSPV(const VkShaderStageFlagBits shaderType,
const char *pshader, std::vector &spirv);
// Kill the shader when not required
void destroyShaders();
// Type of shader language. This could be - EShLangVertex,
// Tessellation Control, Tessellation Evaluation, Geometry,
// Fragment and Compute
EShLanguage getLanguage
(const VkShaderStageFlagBits shaderType);
// Initialize the TBuitInResource
void initializeResources(TBuiltInResource &Resources);
// Vk structure storing vertex & fragment shader information
VkPipelineShaderStageCreateInfo shaderStages[2];
};
用户定义的 VulkanShader 类公开了 buildShader()函数,允许您注入 GLSL 着色器,以便将它们编译到 Vulkan 项目中。 在幕后,该函数利用 GLslang SPIR-V 库函数将它们转换为本地的形式。
该函数分四步构建着色器:
- 使用 glslang :: InitializeProcess()初始化 GLSL 语言着色器库。 请注意,在使用其他任何内容之前,在每个进程中此函数仅需要调用一次。
- 将 GLSL 着色器代码转换为 SPIR-V 着色器字节数组。 这包括以下操作:
- 解析 GLSL 源代码
- 将解析的着色器 shader 添加到程序对象 program object
- 链接到项目对象 project object
- 使用 glslang :: GlslangToSpv()将编译后的二进制着色器转换为 SPIR-V 格式
- 使用转换后的 SPIR-V 着色器中间二进制代码创建着色器模块 — 利用 vkCreateShaderModule()。
- 在结束过程中,使用 glslang :: FinalizeProcess()来完成处理。 每个进程必须调用一次该函数。
该函数使用两个参数作为输入,指定顶点着色器和片段着色器。 这两个着色器用于在 vkCreateShaderModule()API 的帮助下创建着色器模块 VkShaderModule。 该 API 将 VkPipelineShaderStageCreateInfo 控制结构作为主输入,其中包含了着色器所需的信息。
有关更多信息,请参照此处给出的语法和参数说明:
VKAPI_ATTR VkResult VKAPI_CALL vkCreateShaderModule( VkDevice device,
const VkShaderModuleCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkShaderModule* pShaderModule);
下表描述了该函数的各个参数:
参数 | 描述 —|--- device | 这是着色器模块关联的逻辑设备句柄。 pCreateInfo | 这是指向 VkShaderModuleCreateInfo 结构对象的指针。 pAllocator | 这个参数控制着主机内存的分配过程。 有关更多信息,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。 pShaderModule | 这是指创建的 VkShaderModule 对象。
typedef struct VkShaderModuleCreateInfo {
VkStructureType type;
const void* next;
VkShaderModuleCreateFlags flags; size_t codeSize;
const uint32_t* code;
} VkShaderModuleCreateInfo;
下表描述了此结构的各个字段:
字段 | 描述 —|--- type | 这是指结构的类型。 必须是这种类型 VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO。 next | 这或是 NULL 或一个指向扩展特定结构的指针。 flags | 这是保留供将来使用。 codeSize | 这是以字节为单位的源代码大小。 源代码由 code 指向。 code | 这是指用于创建着色器模块的源代码。
以下代码描述了示例中使用的着色器实现。 该实现在 buildShader()函数中完成;此辅助函数目前仅支持顶点和片段着色器,其源代码作为参数传递到这个函数,形式为 GLSL。 以下是具体实现:
/************** VulkanShader.cpp **************/
// Helper function intaking the GLSL vertex and fragment shader.
// It prepares the shaders to be consumed in the SPIR- V format
// with the help of glslang library helper functions.
void VulkanShader::buildShader(const char *vertShaderText, const char
fragShaderText)
{
VulkanDevice deviceObj = VulkanApplication::GetInstance()
->deviceObj;
VkResult result; bool retVal;
// Fill in the control structure to push the necessary
// details of the shader.
std::vector vertexSPV; shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_
SHADER_STAGE_CREATE_INFO;
shaderStages[0].pNext = NULL; shaderStages[0].pSpecializationInfo = NULL; shaderStages[0].flags = 0;
shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; shaderStages[0].pName = “main”;
glslang::InitializeProcess();
retVal = GLSLtoSPV(VK_SHADER_STAGE_VERTEX_BIT,
vertShaderText, vertexSPV);
assert(retVal);
VkShaderModuleCreateInfo moduleCreateInfo; moduleCreateInfo.sType =
VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
moduleCreateInfo.pNext = NULL; moduleCreateInfo.flags = 0; moduleCreateInfo.codeSize = vertexSPV.size() *
sizeof(unsigned int);
moduleCreateInfo.pCode = vertexSPV.data(); result = vkCreateShaderModule(deviceObj->device,
&moduleCreateInfo, NULL, &shaderStages[0].module); assert(result == VK_SUCCESS);
std::vector fragSPV; shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_
SHADER_STAGE_CREATE_INFO;
shaderStages[1].pNext = NULL; shaderStages[1].pSpecializationInfo = NULL; shaderStages[1].flags = 0;
shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; shaderStages[1].pName = “main”;
retVal = GLSLtoSPV(VK_SHADER_STAGE_FRAGMENT_BIT,
fragShaderText, fragSPV);
assert(retVal);
moduleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER
_MODULE_CREATE_INFO;
moduleCreateInfo.pNext = NULL; moduleCreateInfo.flags = 0; moduleCreateInfo.codeSize = fragSPV.size() *
sizeof(unsigned int); moduleCreateInfo.pCode = fragSPV.data();
result = vkCreateShaderModule(deviceObj->device, &moduleCreateInfo, NULL, &shaderStages[1].module);
assert(result == VK_SUCCESS);
glslang::FinalizeProcess();
}
vkCreateShaderModule()将 SPIR-V 格式作为输入;因此,我们需要 GLslang 辅助函数将 GLSL 着色器转换为 SPIR-V 支持的本地格式的二进制形式。
在调用任何 glslang 函数之前,每个进程需要初始化一次库。 这通过调用 glslang :: InitializeProcess()来完成。 接下来,使用用户定义的函数 VulkanShader :: GLSLtoSPV()将输入着色器转换为 SPIR-V 的比特位流,如以下代码所示:
bool VulkanShader::GLSLtoSPV(const VkShaderStageFlagBits shader_type, const char
*pshader, std::vector &spirv)
{
glslang::TProgram& program = *new glslang::TProgram; const char *shaderStrings[1];
TBuiltInResource Resources; initializeResources(Resources);
// Enable SPIR- V and Vulkan rules when parsing GLSL
EShMessages messages = (EShMessages)(EShMsgSpvRules
| EShMsgVulkanRules);
EShLanguage stage = findLanguage(shader_type); glslang::TShader* shader = new glslang::TShader(stage);
shaderStrings[0] = pshader;
shader->setStrings(shaderStrings, 1);
if (!shader->parse(&Resources, 100, false, messages)) { puts(shader->getInfoLog());
puts(shader->getInfoDebugLog()); return false;
}
program.addShader(shader);
// Link the program and report if errors…
if (!program.link(messages)) { puts(shader->getInfoLog()); puts(shader->getInfoDebugLog()); return false;
}
glslang::GlslangToSpv(*program.getIntermediate(stage), spirv);
return true;
}
需要为每种类型的着色器执行编译 GLSLtoSPV 函数,以便将其 GLSL 源代码转换为 SPIR-V 格式。 首先它创建一个空着色器对象并初始化着色器资源的结构。 使用传递给 GLSLtoSPV 函数参数的着色器类型,利用 findLanguage()辅助函数(请参见下面的代码片段)来确定着色器的语言,并创建一个 TShader 着色器对象。 将 GLSL 着色器源代码传递给该着色器并对其进行解析以检查是否存在潜在的问题。 如果发现错误,就报告错误,帮助用户对其进行纠正:
EShLanguage VulkanShader::findLanguage(const VkShaderStageFlagBits shader_type)
{
switch (shader_type) {
case VK_SHADER_STAGE_VERTEX_BIT:
return EShLangVertex;
case VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT:
return EShLangTessControl;
case VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT:
return EShLangTessEvaluation;
case VK_SHADER_STAGE_GEOMETRY_BIT:
return EShLangGeometry;
case VK_SHADER_STAGE_FRAGMENT_BIT:
return EShLangFragment;
case VK_SHADER_STAGE_COMPUTE_BIT:
return EShLangCompute;
default:
printf(“Unknown shader type specified: %d. Exiting!”, shaderType);
exit(1);
}
}
在程序 program 对象中添加着色器 shader 对象以便对其进行编译和链接。glslang :: GlslangToSpv()API 调用的成功执行后,就会把 GLSL 源程序转换为中间着色器(.spv)的形式。
最后,在退出应用程序时,不要忘记删除着色器。 着色器可以使用 vkDestroyShaderModule()API 进行销毁。 有关更多信息,请参阅以下实现:
void VulkanShader::destroyShaders()
{
VulkanApplication* appObj = VulkanApplication::GetInstance(); VulkanDevice* deviceObj = appObj->deviceObj;
vkDestroyShaderModule(deviceObj->device,
shaderStages[0].module, NULL); vkDestroyShaderModule(deviceObj->device,
shaderStages[1].module, NULL);
}
这是 vkDestroyShaderModule()API 的语法:
VKAPI_ATTR void VKAPI_CALL vkDestroyShaderModule( VkDevice device,
VkShaderModule shaderModule, const VkAllocationCallbacks* allocator);
下表介绍了此 API 的各个参数:
参数 | 描述 —|--- device | 这是用来销毁着色器模块对象的逻辑设备。 shaderModule | 这是需要销毁的着色器模块的句柄。 allocator | 这个参数控制了主机内存的释放过程。 有关更多信息,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
总结
在上一章中介绍了图像资源的创建过程,随后,本章讲解了另一种 Vulkan 资源,称之为缓冲区资源。 我们不仅知道了这个概念,同时还使用它实现了几何顶点缓冲区,并且还研究了渲染通道和帧缓冲区,用来在 Vulkan 中定义一个单元渲染作业。 最后,我们通过介绍 SPIR-V 来结束本章的内容,这是一种指定 Vulkan 中着色器和计算内核的新方法。 我们以 SPIR-V 格式实现了我们的第一个着色器,在这里我们将顶点和片段着色器输入到 GLSL 中,并使用 Lunar-G SDK 的 glslangValidator 将其转换为 SPIR-V 格式。
在下一章中,我们会介绍描述符和描述符集。 它们是创建的资源和着色器之间的接口。 我们将使用描述符把我们创建的顶点缓冲区资源信息链接到本章实现的 SPIR-V 着色器上。
在下一章中,我们将介绍 Vulkan 中的管线状态管理。 其中我们会通过管线状态(光栅器状态,混合状态以及深度模板状态)的方式来控制硬件设置,并为输出目的设计输入数据。