第 10 章. 描述符 Descriptors 以及 Push 常量 Constant
SnowFox图形学 AI 分布式 操作系统 编译器 架构 后端 多线程2 人赞同了该文章第 10 章. 描述符 Descriptors 以及 Push 常量 Constant
在前一章中,我们在显示输出中渲染了第一个绘图对象。 在本章中,我们将采用先前的实现,并在 Uniforms 的帮助下对渲染的几何图形执行一些 3D 转换。 Uniform 是着色器中可访问的只读数据块,它们的值对于整个绘制调用而言是恒定不变的。
Uniform 由描述符和描述符池进行管理。 描述符有助于将资源与着色器连接起来。 但它可能会经常变化;因此,分配是通过预先分配的描述符缓冲区(称为描述符池 descriptor pool)来执行的。
在本章中,我们还将实现一个 push 常量。 push 常量允许您使用优化的高速路径更新着色器中的常量数据。
我们将涵盖以下主题:
- 理解描述符 descriptors 的概念
- 如何在 Vulkan 中实现 Uniforms
- pushPush 常量 constant 更新
理解描述符 descriptors 的概念
描述符由描述符集 descriptor set 对象组成。 这些对象包含了一组描述符的存储。 描述符集会把给定的资源(例如 uniform 缓冲区,采样的图像,存储的图像等)连接到着色器,帮助它通过布局绑定(使用描述符集布局定义的)来读取和解释传入的资源数据。 例如,使用描述符将资源(如图像纹理,采样器和缓冲区)绑定到着色器。
描述符是不透明的对象,并定义了与着色器进行通信的协议;在幕后,它提供了一种静默的机制,借助位置绑定将资源内存与着色器相关联。
VulkanDescriptor - 用户定义的描述符类
在本章中,我们将介绍一个名为 VulkanDescriptor 的新用户类,并在此处保留与描述符相关的成员变量和函数。 这将有助于把描述符的代码与实现的其余部分隔离,从而提供了一种更简洁、更容易理解描述符功能的方法。
以下是 VulkanDescriptor.h /.cpp 中 VulkanDescriptor 类的头文件声明。 在我们继续讨论各个部分之前,我们会详细讨论这个类中声明的函数和变量的目的和实现。 请参阅内嵌注释以便快速掌握:
// A user define descriptor class implementing Vulkan descriptors
class VulkanDescriptor
{
public:
VulkanDescriptor(); // Constructor
~VulkanDescriptor(); // Destructor
// Creates descriptor pool and allocate descriptor set from it
void createDescriptor(bool useTexture);
// Deletes the created descriptor set object
void destroyDescriptor();
// Defines the descriptor sets layout binding and
// create descriptor layout
virtual void createDescriptorLayout(bool useTexture) = 0;
// Destroy the valid descriptor layout object
void destroyDescriptorLayout();
// Creates the descriptor pool that is used to
// allocate descriptor sets
virtual void createDescriptorPool(bool useTexture) = 0;
// Deletes the descriptor pool
void destroyDescriptorPool();
// Create the descriptor set from the descriptor pool allocated
// memory and update the descriptor set information into it.
virtual void createDescriptorSet(bool useTexture) = 0; void destroyDescriptorSet();
// Creates the pipeline layout to inject into the pipeline
virtual void createPipelineLayout() = 0;
// Destroys the create pipelineLayout
void destroyPipelineLayouts(); public:
// Pipeline layout object
VkPipelineLayout pipelineLayout;
// List of all the VkDescriptorSetLayouts
std::vector<VkDescriptorSetLayout> descLayout;
// Decriptor pool object that will be used
// for allocating VkDescriptorSet object
VkDescriptorPool descriptorPool;
// List of all created VkDescriptorSet
std::vector<VkDescriptorSet> descriptorSet;
// Logical device used for creating the
// descriptor pool and descriptor sets
VulkanDevice* deviceObj;
};
Descriptor set layout 描述符集布局
描述符集布局就是零个或多个描述符绑定的集合。 它提供了一个接口来读取着色器中指定位置的资源。 每个描述符绑定都有一个特殊的类型,表示它正在处理的资源类别,该绑定中的描述符数量,采样器描述符的数组,以及与其相关联的相应着色器阶段, 这些元数据信息在 VkDescriptorSetLayoutBinding 中指定。 下图显示了描述符集的布局,其中包含各种资源的布局绑定,其中每个资源都会使用该描述符布局中唯一标识的绑定号进行指定:
描述符集布局是使用 vkCreateDescriptorSetLayout()API 创建的。 该 API 接受 VkDescriptorSetLayoutCreateInfo 控制结构,其中使用 VkDescriptorSetLayoutBinding 结构指定了零个或多个描述符集的前述元数据信息。 以下是这个函数的语法:
VkResult vkCreateDescriptorSetLayout(
VkDevice device,
const VkDescriptorSetLayoutCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkDescriptorSetLayout* pSetLayout);
以下是在 vkCreateDescriptorSetLayout()API 中定义的参数:
参数 | 描述
—|---
device | 该参数指定负责创建描述符集布局的逻辑设备(VkDevice)。
pCreateInfo | 该参数指定描述符集布局元数据,通过使用一个指向 VkDescriptorSetLayoutCreateInfo 结构对象的指针。
pAllocator | 这控制主机内存的释放。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pSetLayout | 创建的描述符集布局对象,以 VkDescriptorSetLayout 句柄的形式返回。
让我们来了解这里给出的 VkDescriptorSetLayoutCreateInfo 结构:
typedef struct VkDescriptorSetLayoutCreateInfo {
VkStructureType sType;
const void* pNext;
VkDescriptorSetLayoutCreateFlags flags;
uint32_t bindingCount;
const VkDescriptorSetLayoutBinding* pBindings;
} VkDescriptorSetLayoutCreateInfo;
该表中定义了 VkDescriptorSetLayoutCreateInfo 结构的各个字段:
字段 | 描述
—|---
sType | 这是这个控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO。
pNext | 这可以是一个指向扩展特定结构的有效指针或 NULL。
flags | 该字段属于 VkDescriptorSetLayoutCreateFlags 类型,目前尚未使用;它被保留供将来使用。
bindingCount | 这指的是 pBindings 数组中的条目数。
pBindings | 这是一个指向 VkDescriptorSetLayoutBinding 结构数组的指针。
以下是 VkDescriptorSetLayoutBinding 结构的语法:
typedef struct VkDescriptorSetLayoutBinding {
uint32_t binding;
VkDescriptorType descriptorType;
uint32_t descriptorCount;
VkShaderStageFlags stageFlags;
const VkSampler* pImmutableSamplers;
} VkDescriptorSetLayoutBinding;
下表中定义了 VkDescriptorSetLayoutBinding 结构的各个字段:
字段 | 描述
—|---
binding | 这是指示此资源类型的条目的绑定索引,并且此索引必须等于相应着色器阶段中使用的绑定号或索引。
descriptorType | 这表明了用于绑定的描述符的类型。 类型使用 VkDescriptorType 枚举表示。
descriptorCount | 这表示着色器中描述符的数量,是一个数组,它指向绑定中包含的着色器。
stageFlags | 该字段指定哪些着色器阶段可以访问图形和计算状态的数值。 该着色器阶段由 VkShaderStageFlagBits 的位域指示。 如果值为 VK_SHADER_STAGE_ALL,则定义的所有着色器阶段都可以通过指定的绑定访问资源。
pImmutableSamplers | 这是一个指向采样器(由描述符集布局消耗的相应绑定表示)句柄数组的指针。
如果指定的描述符类型 descriptorType 是 VK_DESCRIPTOR_TYPE_SAMPLER 或 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,则该字段用于初始化一组不可变的采样器。 如果 descriptorType 不是这些描述符类型之一,则忽略此字段(pImmutableSamplers)。 一旦不可变的采样器被绑定,它们就不能再次被绑定到集合布局中。 当此字段为 NULL 时,采样器槽就是动态的,采样器句柄必须使用此布局绑定到描述符集。
以下是 VkDescriptorType 枚举的完整集合,表示各种描述符类型。 每种类型的枚举名称都是不言自明的;它们中的每一个都显示了与其相关联的资源类型:
typedef enum VkDescriptorType {
VK_DESCRIPTOR_TYPE_SAMPLER = 0,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER = 1,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE = 2,
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE = 3,
VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER = 4,
VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER = 5,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER = 6,
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER = 7,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC = 8,
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC = 9,
VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT = 10,
} VkDescriptorType;
让我们继续,并在下一小节中实现描述符集。
实现描述符集布局 descriptor set layout
描述符布局在 VulkanDrawable 类的 createDescriptorLayout()函数中实现。 该函数是在 VulkanDescriptor 类中声明的纯虚函数。 VulkanDrawable 类继承 VulkanDescriptor 类。 以下是它的实现:
void VulkanDrawable::createDescriptorLayout(bool useTexture)
{
// Define the layout binding information for the
// descriptor set(before creating it), specify binding point,
// shader type(like vertex shader below), count etc. VkDescriptorSetLayoutBinding layoutBindings[2]; layoutBindings[0].binding = 0; // DESCRIPTOR_SET_BINDING_INDEX layoutBindings[0].descriptorType =
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
layoutBindings[0].descriptorCount = 1; layoutBindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; layoutBindings[0].pImmutableSamplers = NULL;
// If texture is being used then there exists a
// second binding in the fragment shader
if (useTexture)
{
layoutBindings[1].binding = 1; layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
layoutBindings[1].descriptorCount = 1; layoutBindings[1].stageFlags =
VK_SHADER_STAGE_FRAGMENT_BIT;
layoutBindings[1].pImmutableSamplers = NULL;
}
// Specify the layout bind into the VkDescriptorSetLayout-
// CreateInfo and use it to create a descriptor set layout VkDescriptorSetLayoutCreateInfo descriptorLayout = {}; descriptorLayout.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
descriptorLayout.pNext = NULL; descriptorLayout.bindingCount = useTexture ? 2 : 1; descriptorLayout.pBindings = layoutBindings;
VkResult result;
// Allocate required number of descriptor layout objects and
// create them using vkCreateDescriptorSetLayout()
descLayout.resize(numberOfDescriptorSet);
result = vkCreateDescriptorSetLayout(deviceObj->device, &descriptorLayout, NULL, descLayout.data()); assert(result == VK_SUCCESS);
}
在创建描述符集对象之前,需要定义布局绑定。 在前面的实现中创建了两个 VkDescriptorSetLayoutBinding 对象(一个数组)。
第一个布局绑定 layoutBindings [0],用于将 uniform 块与着色器中指定的资源索引进行绑定。 在本例中,我们的顶点着色器中 uniform 块的索引为 0,与 layoutBindings [0] .binding 字段中指定的值相同。 对象的其他字段指示绑定点被附加到的顶点着色器阶段(stageFlags),以及描述符的数量(descriptorCount)被附加为绑定中包含的着色器中的数组。
第二个数组对象 layoutBindings [1] 表示在我们的几何图形中支持纹理的布局绑定;但是,此示例仅实现 uniform 块来演示 3D 变换。 为了使用当前支持纹理的实现,必须将 createDescriptorLayout()函数的 useTexture 标志参数设置为布尔值 true。 在本例中,尽管我们使用了两个描述符集,但只使用了一个,即 useTexture 为 false。 在后续章节中,我们会实现纹理支持。
Destroying the descriptor set layout 销毁描述符集布局
描述符布局可以使用 vkDestroyDescriptorSetLayout()API 销毁。 这是其语法:
void vkDestroyDescriptorSetLayout( VkDevice device,
VkDescriptorSetLayout descriptorSetLayout, const VkAllocationCallbacks* pAllocator);
vkDestroyDescriptorSetLayout()API 采用以下参数:
参数 | 描述
—|---
device | 这是销毁描述符集布局的逻辑设备。 descriptorSetLayout | 这是要销毁的描述符集布局对象。
pAllocator | 这控制主机内存的释放。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
Understanding pipeline layouts 理解管线布局
管线布局允许管线(图形管线或计算管线)访问描述符集。 管线布局对象由描述符集布局和 push 常量范围组成(参考本章中的“push 常量更新”部分),它表示底层管线可以访问的完整资源集。
在使用 vkCreateGraphicsPipelines()API 创建管线对象之前,需要在 VkGraphicsPipelineCreateInfo 结构中提供管线布局对象的信息, 该信息在 VkGraphicsPipelineCreateInfo :: layout 字段中设置。 这是一个强制性的字段。 如果应用程序不使用描述符集,那么您必须创建一个空的描述符布局,并在管线布局中指定它以满足管线对象(VkPipeline)的创建过程。 有关管线创建过程的更多信息,请参阅第 8 章“管线和管线状态管理”中的“创建图形管线”部分。
管线布局可以按顺序包含零个或多个描述符集,每个描述符集都有特定的布局。 布局定义了着色器阶段和着色器资源之间的接口。 下图显示了由多个描述符布局组成的管线布局,其中包含每个资源的各种布局绑定:
Creating a pipeline layout 创建管线布局
管线布局对象可以在 vkCreatePipelineLayout()API 的帮助下创建。 该 API 接受 VkPipelineLayoutCreateInfo 参数,包含描述符集的状态信息。 该 API 会创建一个管线布局。 我们来看看这个 API 的语法:
VkResult vkCreatePipelineLayout(
VkDevice device,
const VkPipelineLayoutCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkPipelineLayout* pPipelineLayout);
vkCreatePipelineLayout 调用的各个参数定义如下:
参数 | 描述
—|---
device | 这表示负责创建管线布局的逻辑设备(VkDevice)。
pCreateInfo | 该参数是管线布局对象的元数据,使用指向 VkPipelineLayoutCreateInfo 结构的指针来指定。
pAllocator | 这控制主机内存的释放。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pPipelineLayout | 在这个 API 成功执行后会返回 VkPipelineLayout 对象的句柄。
Implementing the pipeline layout creation 实现管线布局的创建
VulkanDrawable 类从 VulkanDrawble 实现了 createPipelineLayout()接口,该接口允许 drawable 类根据绘图对象的资源需求实现其自己的实现。
首先,使用 vkCreateDescriptorSetLayout()API 创建的描述符布局对象(descLayout)进行创建以及指定 VkPipelineLayoutCreateInfo(pPipelineLayoutCreateInfo)。 描述符集绑定信息是通过管线内(VkPipeline)的管线布局进行访问的。
创建的 pPipelineLayoutCreateInfo 会被设置到 vkCreatePipelineLayout()API 中,以便创建 pipelineLayout 对象。 在创建管线时,此对象会传递给 VkGraphicsPipelineCreateInfo :: layout 用来创建图形管线对象(VkPipeline):
// createPipelineLayout is a virtual function from
// VulkanDescriptor and defined in the VulkanDrawable class.
// virtual void VulkanDescriptor::createPipelineLayout() = 0;
// Creates the pipeline layout to inject into the pipeline
void VulkanDrawable::createPipelineLayout()
{
// Create the pipeline layout using descriptor layout.
VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; pPipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE-
_LAYOUT_CREATE_INFO;
pPipelineLayoutCreateInfo.pNext = NULL; pPipelineLayoutCreateInfo.pushConstantRangeCount= 0; pPipelineLayoutCreateInfo.pPushConstantRanges = NULL; pPipelineLayoutCreateInfo.setLayoutCount =
numberOfDescriptorSet; pPipelineLayoutCreateInfo.pSetLayouts = descLayout.data();
VkResult result;
result = vkCreatePipelineLayout(deviceObj->device, &pPipelineLayoutCreateInfo, NULL, &pipelineLayout); assert(result == VK_SUCCESS);
}
Destroying the pipeline layout 销毁管线布局
创建的管线布局可以使用 Vulkan 中的 vkDestroyPipelineLayout()API 进行销毁。 以下是它的描述:
void vkDestroyPipelineLayout(
VkDevice device,
VkPipelineLayout pipelineLayout,
const VkAllocationCallbacks* pAllocator);
这里定义了 vkDestroyPipelineLayout 调用的各个参数:
参数 | 描述
—|---
device | 这是用于销毁管线布局对象的 VkDevice 逻辑对象。
pipelineLayout | 这表示需要销毁的管线布局对象(VkPipelineLayout)。
pAllocator | 这控制主机内存的分配。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
让我们使用这个 API 并在下一节中实现它。
实现管线布局的销毁过程 pipeline layout destruction process
VulkanDescriptor 类提供了一个高级函数来销毁创建的管线布局:destroyPipelineLayouts()函数。 以下是代码实现:
// Destroy the create pipeline layout object
void VulkanDescriptor::destroyPipelineLayouts()
{
vkDestroyPipelineLayout(deviceObj->device, pipelineLayout, NULL);
}
Descriptor pool 描述符池
在 Vulkan 中,不能直接创建描述符集;相反,它们首先是从称之为“描述符池”的特殊池中进行分配的。 描述符池负责分配描述符集对象。 换句话说,它是描述符的一个集合,描述符集就是从这里分配的。
注意
描述符池可用于描述符集的多个对象的高效内存分配,而无需全局同步。
Creating a descriptor pool 创建描述符池
创建描述符池非常简单;就是使用 vkCreateDescriptorPool API。 以下是 API 规范,随后在我们的示例片段中实现了此 API:
VkResult vkCreateDescriptorPool(
VkDevice device,
const VkDescriptorPoolCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkDescriptorPool* pDescriptorPool);
这里定义了 vkCreateDescriptorPool 调用的各个参数:
参数 | 描述
—|---
device | 这指定负责创建描述符池的逻辑设备(VkDevice)。
pCreateInfo |该参数是描述符池对象的元数据,该池使用一个指向 VkDescriptorPoolCreateInfo 结构对象的指针来指定。
pAllocator | 这控制主机内存的分配。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pDescriptorPool | 这表示创建的描述符池对象的类型句柄(VkDescriptorPool),这是执行此 API 的结果。
实现描述符池的创建 descriptor pool
createDescriptorPool()是 VulkanDescriptor 类公开的纯虚函数。 该函数在 VulkanDrawble 类中得到了实现,负责在我们的 Vulkan 应用程序示例中创建描述符池。 让我们来了解一下这个函数的工作方式:
- 首先,定义描述符池的尺寸结构,指示描述符池内需要创建的用于分配每种类型的描述符集的池的数量。 在下面的实现中使用了两种类型的描述符集;因此,创建了两个 VkDescriptorPoolSize 对象。 第一个对象指示需要为 uniform 缓冲区描述符类型提供分配存储空间的描述符池。 该池将用于分配绑定到 uniform 块资源类型的描述符集对象。
- 第二个对象指示用于纹理采样器的描述符池。 我们会在下一章中实现纹理。
- 然后,在描述符池的 CreateInfo 结构(descriptorPoolCreateInfo)中指定创建的这些对象(descriptorTypePool),以指示所创建的描述符池所支持的描述符集类型(以及其他的状态信息)。 最后,descriptorTypePool 对象会被 vkCreateDescriptorPool()API 用来创建描述符池对象 descriptorPool。
描述符池的实现在这里给出:
// Creates the descriptor pool, this function depends on -
// createDescriptorSetLayout()
void VulkanDrawable::createDescriptorPool(bool useTexture)
{
VkResult result;
// Define the size of descriptor pool based on the
// type of descriptor set being used.
VkDescriptorPoolSize descriptorTypePool[2];
// The first descriptor pool object is of type Uniform buffer
descriptorTypePool[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; descriptorTypePool[0].descriptorCount = 1;
// If texture is supported then define the second object with
// descriptor type to be Image sampler
if (useTexture){
descriptorTypePool[1].type= VK_DESCRIPTOR_TYPE_-
COMBINED_IMAGE_SAMPLER;
descriptorTypePool[1].descriptorCount = 1;
}
// Populate the descriptor pool state information
// in the create info structure.
VkDescriptorPoolCreateInfo descriptorPoolCreateInfo = {}; descriptorPoolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_-
POOL_CREATE_INFO;
descriptorPoolCreateInfo.pNext = NULL;
descriptorPoolCreateInfo.maxSets = 1;
descriptorPoolCreateInfo.poolSizeCount= useTexture ? 2 : 1; descriptorPoolCreateInfo.pPoolSizes = descriptorTypePool;
// Create the descriptor pool using the descriptor
// pool create info structure
result = vkCreateDescriptorPool(deviceObj->device, &descriptorPoolCreateInfo, NULL, &descriptorPool);
assert(result == VK_SUCCESS);
}
Destroying the descriptor pool 销毁描述符池
描述符池可以使用 vkDestroyDescriptorPool()API 进行销毁。 这个 API 接受三个参数。 第一个参数 device 指定拥有描述符池并将用于销毁 descriptorPool 的逻辑设备(VkDevice)。 第二个参数 descriptorPool 是需要使用此 API 销毁的描述符池对象。 最后一个参数 pAllocator 控制主机内存的分配。 有关更多信息,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的”主机内存“部分:
void vkDestroyDescriptorPool(
VkDevice device,
VkDescriptorPool descriptorPool, const VkAllocationCallbacks* pAllocator);
destruction of the descriptor pool 实现描述符池的销毁
在本示例程序中,使用了来自 VulkanDescriptor 的 desctroyDescriptorPool()函数销毁创建的描述符池对象:
// Deletes the descriptor pool
void VulkanDescriptor::destroyDescriptorPool()
{
vkDestroyDescriptorPool(deviceObj->device, descriptorPool, NULL);
}
Creating the descriptor set resources 创建描述符集资源
在创建描述符集之前,必须创建资源才能将其与描述符集关联或绑定。 在本节中,我们会创建一个 uniform 缓冲区资源,稍后将其与我们在后面创建的描述符集相关联 — 位于“创建描述符集”小节。
所有与描述符相关的资源都是在 VulkanDescriptor 类的 createDescriptorResources()接口中创建的。 根据需求,可以在派生类中实现这个接口。
在本示例中,在 VulkanDrawable 类中实现了该接口,此类会创建一个 uniform 缓冲区并将 4 x 4 转换存储到类中。 为此,我们需要创建缓冲区类型的资源。 请记住,Vulkan 中有两种类型的资源:缓冲区和图像。 我们在第 7 章“缓冲区资源,渲染通道,帧缓冲区和使用 SPIR-V 的着色器”中的“理解缓冲区资源”部分中创建了缓冲区资源。 在同一章节中,我们创建了顶点缓冲区(参见“使用缓冲区资源创建几何图形”部分)。 我们将重用本章的学习内容,并实现一个 uniform 缓冲区来存储 uniform 块的信息。
以下代码实现了 createDescriptorResources(),它会在其中调用另一个函数 createUniformBuffer(),该函数创建 uniform 缓冲区资源:
// Create the Uniform resource inside. Create Descriptor set
// associated resources before creating the descriptor set
void VulkanDrawable::createDescriptorResources()
{
createUniformBuffer();
}
createUniformBuffer()函数使用 glm 库的辅助函数生成转换矩阵信息。 它根据用户的要求计算正确的模型,视图和投影矩阵,并将结果存储在 MVP 矩阵中。 MVP 存储在主机内存中,需要使用缓冲对象(VkBuffer)将其传送到设备内存。 以下是创建 MVP 的缓冲区资源(VkBuffer)的步骤说明:
- 创建缓冲区对象 buffer object:使用 vkCreateBuffer()API 创建一个 VkBuffer 对象(UniformData.buffer)。 此 API 会提取一个 VkCreateBufferInfo 结构对象(bufInfo),该对象指定了用于创建缓冲区对象的重要缓冲区元数据。 例如,它会将 bufInfo.usage 中的 usage 类型指示为 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,因为 MVP 被视为顶点着色器中的 uniform 块资源。 它需要的另一个重要信息是缓冲区的大小;这会用来保存完整的 MVP 缓冲区信息。 在当前情况下,它等于 4×4 变换矩阵的大小。 在这个阶段,当创建了缓冲区对象时(UniformData.buffer),并没有物理存储的支持与它相关联。 为了分配实际的物理内存,请继续下一步。
- Allocating physical memory for the buffer resource 为缓冲区资源分配物理内存:
- 获取内存要求:Get the memory requirements,分配缓冲区资源所需的适当大小的内存。 通过将 VkBuffer 对象传递给 vkGetBufferMemoryRequirements API 来查询基本的内存情况。 这会将所需的内存信息返回到 VkMemoryRequirements 类型对象中(memRqrmnt)。
- 确定内存类型:Determining the memory type,从可用的选项中获取适当的内存类型,然后选择与用户属性匹配的内存类型。
- 分配设备内存:Allocating device memory,使用 vkAllocateMemory()API 为缓冲区资源分配物理内存(位于 UniformData :: memory 中,类型为 VkDeviceMemory)。
- 映射设备内存:Mapping the device memory,使用 vkMapMemory()API 将物理设备内存映射到应用程序的地址空间。 将 uniform 缓冲区数据上传到该地址空间。 通过使用 vkInvalidateMappedMemoryRanges()使映射的缓冲区无效,以使其对主机可见。 如果内存属性设置为 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,则驱动程序就会处理这个问题;否则,对于非连贯映射的内存,需要显式调用 vkInvalidateMappedMemoryRanges()。
- 绑定分配的内存:Binding the allocated memory,使用 vkBindBufferMemory()API 将设备内存(UniformData :: memory)绑定到缓冲区对象(UniformData :: buffer)。
下图提供了所述过程的概述:
一旦创建了缓冲区资源,它就会将必要的信息存储在本地数据结构中以实现管理目的:
class VulkanDrawable : public VulkanDescriptor
{
. . . .
// Local data structure for uniform buffer house keeping
struct {
// Buffer resource object
VkBuffer buffer;
// Buffer resourece object’s allocated device memory
VkDeviceMemory memory;
// Buffer info that need to supplied into
// write descriptor set (VkWriteDescriptorSet)
VkDescriptorBufferInfo bufferInfo;
// Store the queried memory requirement
// of the uniform buffer
VkMemoryRequirements memRqrmnt;
// Metadata of memory mapped objects
std::vector<VkMappedMemoryRange> mappedRange;
// Host pointer containing the mapped device
// address which is used to write data into.
uint8_t* pData;
} UniformData;
. . . .
};
void VulkanDrawable::createUniformBuffer()
{
VkResult result; bool pass;
Projection = glm::perspective(radians(45.f), 1.f, .1f, 100.f); View = glm::lookAt(
glm::vec3(10, 3, 10), // Camera in World Space glm::vec3(0, 0, 0), // and looks at the origin glm::vec3(0, -1, 0) );// Head is up
Model = glm::mat4(1.0f);
MVP = Projection * View * Model;
// Create buffer resource states using VkBufferCreateInfo
VkBufferCreateInfo bufInfo = {};
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufInfo.pNext = NULL;
bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; bufInfo.size = sizeof(MVP); bufInfo.queueFamilyIndexCount = 0; bufInfo.pQueueFamilyIndices = NULL; bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; bufInfo.flags = 0;
// Use create buffer info and create the buffer objects
result = vkCreateBuffer(deviceObj->device, &bufInfo, NULL, &UniformData.buffer);
assert(result == VK_SUCCESS);
// Get the buffer memory requirements VkMemoryRequirements memRqrmnt; vkGetBufferMemoryRequirements(deviceObj->device,
UniformData.buffer, &memRqrmnt);
VkMemoryAllocateInfo memAllocInfo = {};
memAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; memAllocInfo.pNext = NULL; memAllocInfo.memoryTypeIndex = 0; memAllocInfo.allocationSize = memRqrmnt.size;
// Determine the type of memory required
// with the help of memory properties
pass = deviceObj->memoryTypeFromProperties (memRqrmnt.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
&memAllocInfo.memoryTypeIndex); assert(pass);
// Allocate the memory for buffer objects
result = vkAllocateMemory(deviceObj->device, &memAllocInfo, NULL, &(UniformData.memory));
assert(result == VK_SUCCESS);
// Map the GPU memory on to local host
result = vkMapMemory(deviceObj->device, UniformData.memory, 0, memRqrmnt.size, 0, (void **)&UniformData.pData); assert(result == VK_SUCCESS);
// Copy computed data in the mapped buffer
memcpy(UniformData.pData, &MVP, sizeof(MVP));
// We have only one Uniform buffer object to update
UniformData.mappedRange.resize(1);
// Populate the VkMappedMemoryRange data structure
UniformData.mappedRange[0].sType =
VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
UniformData.mappedRange[0].memory = UniformData.memory; UniformData.mappedRange[0].offset = 0; UniformData.mappedRange[0].size = sizeof(MVP);
// Invalidate the range of mapped buffer in order
// to make it visible to the host.
vkInvalidateMappedMemoryRanges(deviceObj->device, 1, &UniformData.mappedRange[0]);
// Bind the buffer device memory
result = vkBindBufferMemory(deviceObj->device, UniformData.buffer, UniformData.memory, 0);
assert(result == VK_SUCCESS);
// Update the local data structure with uniform
// buffer for house keeping UniformData.bufferInfo.buffer = UniformData.buffer; UniformData.bufferInfo.offset = 0; UniformData.bufferInfo.range = sizeof(MVP); UniformData.memRqrmnt = memRqrmnt;
}
接下来,我们会创建描述符集并将创建的 uniform 缓冲区与它建立关联。
Creating the descriptor sets 创建描述符集
描述符集的创建过程包括两个步骤:
- 描述符集的分配:Descriptor set allocation,这指的是从描述符池中分配描述符集。
- 资源配置:Resource assignment,在这里,描述符集会与创建的资源数据相关联。
从描述符池分配描述符集对象 Allocating the descriptor set object from the descriptor pool
从描述符池分配描述符集对象
描述符集是使用 vkAllocateDescriptorSets()API 从描述符池进行分配的。 该 API 采用三个参数。 第一个参数(device)指定拥有描述符池的逻辑设备(类型为 VkDevice)。 第二个参数(pAllocateInfo)是一个指向 VkDescriptorSetAllocateInfo 结构对象的指针,该结构描述了在描述符池的分配过程中有用的各种参数。 最后一个参数(pDescriptorSets)是一个指向 VkDescriptorSet 数组的指针;这会由该 API 进行填充 – 使用分配的描述符集的句柄:
VkResult vkAllocateDescriptorSets(
VkDevice device,
const VkDescriptorSetAllocateInfo* pAllocateInfo, VkDescriptorSet* pDescriptorSets);
Destroying the allocated descriptor set objects 销毁分配的描述符集对象
分配的描述符集对象可以使用 vkFreeDescriptorSets()API 来释放。 该 API 接受四个参数。 第一个参数(device)是拥有描述符池的逻辑设备。 第二个设备是用于分配描述符集的描述符池(descriptorPool)。 第三个参数(descriptorSetCount)表示最后一个参数中元素的数量。 最后一个参数(pDescriptorSets)是需要释放的 VkDescriptorSet 对象的数组:
VkResult vkFreeDescriptorSets( VkDevice device,
VkDescriptorPool descriptorPool,
uint32_t descriptorSetCount,
const VkDescriptorSet* pDescriptorSets);
在当前的示例实现中,vkFreeDescriptorSets()API 通过 VulkanDescriptor 类中的 destroyDescriptorSet()辅助函数来对外公开接口。 以下是实现代码:
void VulkanDescriptor::destroyDescriptorSet()
{
vkFreeDescriptorSets(deviceObj->device, descriptorPool, numberOfDescriptorSet, &descriptorSet[0]);
}
Associating the resources with the descriptor sets 关联资源和描述符集
可以通过使用 vkUpdateDescriptorSets()API 更新描述符集,从而将描述符集与资源信息关联起来。 该 API 使用四个参数。 第一个参数 device 是用来更新描述符集的逻辑设备,这个逻辑设备应该是拥有描述符集的设备。 第二个参数 descriptorWriteCount 指定(VkCopyDescriptorSettype 类型)pDescriptorWrites 数组中的元素数量。 第三个参数 pDescriptorWrites 是一个指向 pDescriptorCopies 数组中的 VkWriteDescriptorSet 数组的指针。 最后一个参数 pDescriptorCopies 是一个指向数组对象(VkCopyDescriptorSet 结构)的指针,用于描述彼此之间要拷贝的描述符集:
void vkUpdateDescriptorSets(
VkDevice device,
uint32_t descriptorWriteCount,
const VkWriteDescriptorSet* pDescriptorWrites,
uint32_t descriptorCopyCount,
const VkCopyDescriptorSet* pDescriptorCopies);
更新是两个操作的合并,即写入和复制:
- 写入:通过填充带有资源信息(例如缓冲区数据,计数,绑定索引等)的零个或多个 VkWriteDescriptorSet 控制结构的数组来更新分配的描述符集。 写入操作在 vkUpdateDescriptorSets()API 中指定。 该 API 使用填充后的 VkWriteDescriptorSet 数据结构。
- 复制:复制操作使用现有的描述符集并将其信息复制到目标描述符集。 复制操作由 VkWriteDescriptorSet 控制结构指定。 可能有零个或多个写入操作。
注意
写操作首先执行,然后是复制操作。 对于每种操作类型(写入或复制),零或多个操作以数组的形式表示,并且在这些数组内,操作按照它们出现的顺序执行。
以下是 VkWriteDescriptorSet 的定义:
typedef struct VkWriteDescriptorSet {
VkStructureType sType;
const void* pNext;
VkDescriptorSet dstSet;
uint32_t dstBinding;
uint32_t dstArrayElement;
uint32_t descriptorCount;
VkDescriptorType descriptorType;
const VkDescriptorImageInfo* pImageInfo;
const VkDescriptorBufferInfo* pBufferInfo;
const VkBufferView* pTexelBufferView;
} VkWriteDescriptorSet;
VkWriteDescriptorSet 结构的各个字段定义如下:
字段 | 描述
—|---
sType | 这是这个控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET。
pNext | 这可以是一个指向扩展特定结构的有效指针或 NULL。
dstSet | 这是将要更新的目标描述符集。
dstBinding | 这指定了集合内的描述符绑定。 对于一个给定的着色器阶段,这应该与着色器中指定的绑定索引相同。
dstArrayElement | 该字段表示单个绑定内描述符数组中的起始元素索引。 descriptorCount | 这是以下任何一种中要更新的描述符的数量:pImageInfo,pBufferInfo 或 pTexelBufferView。
descriptorType | 该字段指示每个参与的描述符的类型(pImageInfo,pBufferInfo 或 pTexelBufferView)。
pImageInfo | 这是表示图像资源的一个 VkDescriptorImageInfo 结构数组。 如果未指定,该字段必须是 VK_NULL_HANDLE。
pBufferInfo | 这是一个 VkDescriptorBufferInfo 结构数组,或者如果未指定,它可以是 VK_NULL_HANDLE。
pTexelBufferView | 这是一个包含 VkBufferView 句柄的数组,如果未指定,它可以是 VK_NULL_HANDLE。
我们来看看 VkCopyDescriptorSet 的定义:
typedef struct VkCopyDescriptorSet {
VkStructureType sType;
const void* pNext;
VkDescriptorSet srcSet;
uint32_t srcBinding;
uint32_t srcArrayElement;
VkDescriptorSet dstSet;
uint32_t dstBinding;
uint32_t dstArrayElement;
uint32_t descriptorCount;
} VkCopyDescriptorSet;
这里定义了 VkCopyDescriptorSet 结构的各个字段:
字段 | 描述
—|---
sType | 这是这个控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET。
pNext | 这可以是一个指向扩展特定结构的有效指针或 NULL。
srcSet | 这指定了要从中进行复制的源描述符集。
srcBinding | 指定了源描述符集内的绑定索引。
srcArrayElement | 这表示第一个更新的绑定中的起始数组元素。
dstSet | 这指定了源描述符要被复制到的目标描述符集。
dstBinding | 这指定了目标描述符集内的绑定索引。
dstArrayElement | 该字段表示一个绑定内描述符数组中的起始索引。
descriptorCount | 该字段是指要从源复制到目标的描述符总数。
Implementing descriptor set creation 实现描述符集的创建
描述符集在 VulkanDrawable 类中创建,该类继承 VulkanDescriptor 类的 createDescriptorSet()接口并对其进行实现。
首先,在描述符池中创建并指定 VkDescriptorSetAllocateInfo 控制结构(dsAllocInfo),以便从预期的描述符池中分配 descriptorSet 。 需要指定的第二件重要的事情就是我们创建并存储在 descLayout 对象中的描述符布局信息。 描述符布局提供了一个接口来读取着色器中的资源。
分配的描述符集是空的并且不包含任何有效信息。 它们使用写入或复制描述符结构进行更新(Vk DescriptorSet)。 在这个实现中,写描述符 write [0] 以及其他的状态信息一起使用 uniform 数据缓冲区(UniformData :: bufferInfo)进行指定。 该信息包括目标描述符集对象 descriptorSet [0],这个 uniform 缓冲区需要绑定到该目标描述符集对象,并且它(目标描述符集对象)应该被附加到目标绑定索引。 dstBinding 必须等于着色器阶段中指定的索引。 使用 vkUpdateDescriptorSets()执行更新操作,并且对它指定了 write 描述符:
// Creates the descriptor sets using descriptor pool.
// This function depends on the createDescriptorPool()
// and createUniformBuffer().
void VulkanDrawable::createDescriptorSet(bool useTexture)
{
VulkanPipeline* pipelineObj = rendererObj->getPipelineObject(); VkResult result;
// Create the descriptor allocation structure and specify
// the descriptor pool and descriptor layout VkDescriptorSetAllocateInfo dsAllocInfo[1]; dsAllocInfo[0].sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
dsAllocInfo[0].pNext = NULL; dsAllocInfo[0].descriptorPool = descriptorPool; dsAllocInfo[0].descriptorSetCount = 1; dsAllocInfo[0].pSetLayouts = descLayout.data();
// Allocate the number of descriptor set needs to be produced
descriptorSet.resize(1);
// Allocate descriptor sets
result = vkAllocateDescriptorSets(deviceObj->device,
dsAllocInfo, descriptorSet.data()); assert(result == VK_SUCCESS);
// Allocate two write descriptors for - 1. MVP and 2. Texture
VkWriteDescriptorSet writes[2]; memset(&writes, 0, sizeof(writes));
// Specify the uniform buffer related
// information into first write descriptor
writes[0] = {};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].pNext = NULL; writes[0].dstSet = descriptorSet[0]; writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[0].pBufferInfo = &UniformData.bufferInfo; writes[0].dstArrayElement = 0;
writes[0].dstBinding = 0;
// If texture is used then update the second
// write descriptor structure. We will use this descriptor
// set in the next chapter where textures are used.
if (useTexture)
{
// In this sample textures are not used
writes[1] = {};
writes[1].sType = VK_STRUCTURE_TYPE_WRITE-
DESCRIPTOR_SET;
writes[1].dstSet = descriptorSet[0];
writes[1].dstBinding = 1;
writes[1].descriptorCount = 1; writes[1].descriptorType = VK_DESCRIPTOR_TYPE-
COMBINED_IMAGE_SAMPLER;
writes[1].pImageInfo = NULL; writes[1].dstArrayElement = 0;
}
// Update the uniform buffer into the allocated descriptor set
vkUpdateDescriptorSets(deviceObj->device, useTexture ? 2 : 1, writes, 0, NULL);
}
如何在 Vulkan 中实现 Uniform?
在本节中,我们将了解 Vulkan 中 Uniform 实现的需求和执行模型。 我们还会介绍使用 Uniform 在渲染对象上应用 3D 变换的步骤说明。 重用前一章中的示例片段,并按照给定的说明进行操作。
Prerequisites 准备阶段
我们先看看需求。
3D 变换:3D Transformation,该实现使用到了 glm 数学库,使用库的内置变换函数实现 3D 变换。 GLM 是基于 GLSL 规范,用于图形软件的头文件 C ++ 数学库。 你可以从 http://glm.g-truc.net 下载这个库。 要想使用它,请执行以下更改:
- CMakeLists.txt:通过将以下行添加到项目的 CMakeLists.txt 文件来加入 GLM 支持:
# GLM SETUP
set (EXTDIR “CMAKESOURCEDIR/../../external")set(GLMINCLUDES"{EXTDIR}”)
get_filename_component(GLMINC_PREFIX “${GLMINCLUDES}” ABSOLUTE) if(NOT EXISTS ${GLMINC_PREFIX})
message(FATAL_ERROR "Necessary glm headers do not exist: "
${GLMINC_PREFIX})
endif()
include_directories( ${GLMINC_PREFIX} )
- 头文件:将头文件包含在 Headers.h 文件中:
/*********** GLM HEADER FILES ***********/
#define GLM_FORCE_RADIANS
#include “glm/glm.hpp”
#include <glm/gtc/matrix_transform.hpp>
应用转换:Applying transformations,转换会在渲染发生之前执行。 在当前设计中引入了一个 update()函数,并在 render()函数执行之前对其进行调用。 将 update()添加到 VulkanRenderer 和 VulkanDrawable 中,并按如下方式实现 main.cpp:
int main(int argc, char *argv)
{
VulkanApplication appObj = VulkanApplication::GetInstance(); appObj->initialize();
appObj->prepare();
bool isWindowOpen = true; while (isWindowOpen) {
// Add the update function here…
appObj->update();
isWindowOpen = appObj->render();
}
appObj->deInitialize();
}
描述符 descriptor 类:VulkanDrawable 类继承 VulkanDescriptor,将所有描述符相关的辅助函数和用户变量放在一起,但要保持代码逻辑分离。 同时,它允许不同的绘图实现类根据它们的需求对其进行扩展:
Execution model overview 执行模型概述
本节将帮助我们理解 Uniforms 的执行模型 ------ 就是使用 Vulkan 中的描述符集。 以下是步骤说明:
- 初始化:Initialization,当应用程序初始化时,它会调用渲染器的 initialize()函数。 该函数创建与每个可绘制对象关联的所有描述符。 VulkanDrawable 类是从 VulkanDescriptor 继承的,其中包含描述符集和描述符池以及相关的辅助函数。 描述符集是从描述符池中进行分配的。
- 创建描述符布局:Creating the descriptor layout,描述符布局定义了描述符绑定。 该绑定指示描述符相关的元数据,例如它与哪种着色器相关联,描述符的类型,着色器中的绑定索引以及此类描述符的总数。
- 管线布局:The pipeline layout,创建管线布局;描述符集是通过管线布局在管线对象中指定的。
- 为转换创建一个 Uniform 的缓冲区:Creating a uniform buffer for the transformation,转换信息在一个 4 x 4 转换矩阵中指定。 这是在设备内存的 Uniform 缓冲区中创建的(createUniformBuffer()),顶点着色器使用该缓冲区读取转换信息并将其应用到几何图形的顶点。
- 创建描述符池:Creating the descriptor pool:,接下来,创建一个描述符池,描述符集将会从其中进行分配。
- 创建描述符集:Creating the descriptor set,从创建的描述符池中分配描述符集(步骤 5),并将 Uniform 缓冲区数据(在步骤 4 中创建)与之关联。
- 更新变换:Updating the transformation,在每个帧中更新变换,其中使用了新的变换数据内容映射和更新 Uniform 缓冲区 GPU 内存。
Initialization 初始化
初始化包括顶点和片段着色器的实现,构建 Uniform 缓冲区资源以及从描述符池创建描述符集。 描述符集的创建过程包括构建描述符和管线布局。
Shader implementation 着色器实现
通过一个顶点着色器来应用该转换 – 使用 Uniform 缓冲区作为输入接口 — 通过布局绑定索引为 1 的 Uniform 区块(bufferVals),正如以下代码中以粗体形式突出显示的那样。
该转换是通过模型视图投影矩阵 bufferVals-mvp(layout bingding= 0)和输入顶点 pos(layout location= 0)的乘积来计算的:
// Vertex shader
#version 450
layout (std140, binding = 0) uniform bufferVals { mat4 mvp;
} myBufferVals;
layout (location = 0) in vec4 pos; layout (location = 1) in vec4 inColor; layout (location = 0) out vec4 outColor; void main() {
outColor = inColor;
gl_Position = myBufferVals.mvp * pos;
gl_Position.z = (gl_Position.z + gl_Position.w) / 2.0;
}
片段着色器中不需要做什么更改。 在 location 0 处接收的输入颜色(color)用作输出 location 0(outColor)指定的当前片段的颜色:
// Fragment shader
#version 450
layout (location = 0) in vec4 color; layout (location = 0) out vec4 outColor; void main() {
outColor = color;
}
Creating descriptors 创建描述符
当渲染器被初始化时(使用 initialize()函数),描述符在名为 createDescriptors()的辅助函数中创建。 该函数首先通过调用 VulkanDrawable 的 createDescriptorSetLayout()函数为每个可绘制的对象创建描述符布局。 接下来,描述符对象在 VulkanDrawable 的 createDescriptor()函数中创建。 在这个例子中,我们不是对纹理进行编程;因此,我们将参数值作为 Boolean false 发送:
// Create the descriptor sets
void VulkanRenderer::createDescriptors()
{
for each (VulkanDrawable* drawableObj in drawableList)
{
// It is up to an application how it manages the
// creation of descriptor. Descriptors can be cached
// and reuse for all similar objects.
drawableObj->createDescriptorSetLayout(false);
// Create the descriptor set
drawableObj->createDescriptor(false);
}
}
void VulkanRenderer::initialize()
{
. . . .
// Create the vertex and fragment shader createShaders();
// Create descriptor set layout createDescriptors();
// Manage the pipeline state objects createPipelineStateManagement();
. . . .
}
在创建图形管线布局之前,必须先执行 createDescriptorSetLayout()函数。 这可以确保在 VulkanDrawable :: createPipelineLayout()函数中创建管线布局时能够正确使用描述符布局。 有关 createPipelineLayout()的更多信息,请参阅本章”管线布局“小节中的“实现管线布局的创建”部分。
描述符集的创建包括三个步骤 - 首先,创建 Uniform 缓冲区;其次,创建描述符池;最后,分配描述符集,并使用 Uniform 缓冲区资源更新描述符集:
void VulkanDescriptor::createDescriptor(bool useTexture)
{
// Create the uniform buffer resource
createDescriptorResources();
// Create the descriptor pool and
// use it for descriptor set allocation
createDescriptorPool(useTexture);
// Create descriptor set with uniform buffer data in it
createDescriptorSet(useTexture);
}
有关创建 Uniform 资源的更多信息,请参阅本章中的“创建描述符集资源”部分。 另外,您可以参考“创建描述符池”和“创建描述符集”小节来获得描述符池和描述符集的创建相关的详细内容。.
Rendering 渲染
创建的描述符集需要在绘图对象中指定。 这是在记录绘图对象的命令缓冲区时完成的(VulkanDrawable :: recordCommandBuffer())。
使用 recordCommandBuffer()中的 vkCmdBindDescriptorSets()API 把描述符集和记录的命令缓冲区进行绑定。 在使用当前命令缓冲区绑定管线对象(vkCmdBindPipeline())之后以及在绑定顶点缓冲区(vkCmdBindVertexBuffers())API 之前调用此 API:
void VulkanDrawable::recordCommandBuffer(int currentBuffer,
VkCommandBuffer* cmdDraw)
{
// Bound the command buffer with the graphics pipeline vkCmdBindPipeline(*cmdDraw, VK_PIPELINE_BIND_POINT_GRAPHICS,
*pipeline);
// Bind the descriptor set into the command buffer vkCmdBindDescriptorSets(*cmdDraw,VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, descriptorSet.data(), 0, NULL);
const VkDeviceSize offsets[1] = { 0 }; vkCmdBindVertexBuffers(*cmdDraw, 0, 1,
&VertexBuffer.buf, offsets);
. . . .
}
有关 vkCmdBindDescriptorSets()API 规范的更多信息,请参阅以下小节,“绑定描述符集”。
Binding the descriptor set 绑定描述符集
可以使用 vkCmdBindDescriptorSets()在命令缓冲区中指定已创建的一个或多个描述符集:
void vkCmdBindDescriptorSets(
VkCommandBuffer commandBuffer,
VkPipelineBindPoint pipelineBindPoint,
VkPipelineLayout layout,
uint32_t firstSet,
uint32_t descriptorSetCount,
const VkDescriptorSet* pDescriptorSets,
uint32_t dynamicOffsetCount,
const uint32_t* pDynamicOffsets);
vkCmdBindDescriptorSets 调用的各个参数定义如下:
参数 | 描述
—|---
commandBuffer | 这是描述符集要绑定到的命令缓冲区(VkCommandBuffer)。
pipelineBindPoint | 该参数是类型为 VkPipelineBindPoint 的管线的绑定点,指示描述符是否会被图形管线或计算管线使用。 图形和计算管线的各个绑定点不会干扰彼此的工作。
Layouts | 这指的是用于 program 绑定的 VkPipelineLayout 对象。
firstSet |这表示要绑定的第一个描述符集的索引。
descriptorSetCount | 这指的是 pDescriptorSets 数组中元素的数量。
pDescriptorSets | 这是一个 VkDescriptorSet 对象(描述了要写入的描述符集)的句柄数组。
dynamicOffsetCount | 是指 pDynamicOffsets 数组中的动态偏移数量。 pDynamicOffsets | 这是一个指向 uint32_t 数值(指定了动态的偏移量)的数组指针。
Update 更新
一旦命令缓冲区(与描述符集绑定的)被提交给队列,它就会执行并使用 Uniform 缓冲区中指定的变换渲染绘图对象。 为了更新和渲染连续的更新变换,可以使用 update()函数。
注意
更新描述符集可能是一条性能关键路线;因此,建议根据更新频率划分多个描述符。 它可以分为场景级别,模型级别以及绘图级别,其中更新频率分别为低,中和高。
Updating the transformation 更新变换
在 drawable 类(VulkanDrawable)的 update()函数中,会在每一帧中更新变换操作,该函数获取 Uniform 缓冲区的内存位置并使用新信息更新变换矩阵。 Uniform 缓冲区内存位置不可直接使用,因为它是 GPU 内存的常驻内存;因此,通过内存映射来分配 GPU 内存,其中 GPU 内存的一部分会被映射到 CPU 内存。 一旦内存更新,它就会重新映射到 GPU 内存。 以下代码片段实现了 update()函数:
void VulkanDrawable::update()
{
VulkanDevice* deviceObj = rendererObj->getDevice(); uint8_t *pData;
glm::mat4 Projection = glm::perspective(glm::radians(45.0f), 1.0f,
0.1f, 100.0f);
glm::mat4 View = glm::lookAt(
glm::vec3(0, 0, 5), // Camera is in World Space
glm::vec3(0, 0, 0), // and looks at the origin
glm::vec3(0, 1, 0)); // Head is up
glm::mat4 Model = glm::mat4(1.0f); static float rot = 0;
rot += .003;
Model = glm::rotate(Model, rot, glm::vec3(0.0, 1.0, 0.0))
- glm::rotate(Model, rot, glm::vec3(1.0, 1.0, 1.0));
// Compute the ModelViewProjection transformation matrix
glm::mat4 MVP = Projection * View * Model;
// Map the GPU memory on to local host
VkResult result = vkMapMemory(deviceObj->device, UniformData. memory, 0, UniformData.memRqrmnt.size, 0,
(void **)&pData);
assert(result == VK_SUCCESS);
// The device memory we have kept mapped it,
// invalidate the range of mapped buffer in order
// to make it visible to the host.
VkResult res = vkInvalidateMappedMemoryRanges(deviceObj->device, 1, &UniformData.mappedRange[0]);
assert(res == VK_SUCCESS);
// Copy computed data in the mapped buffer
memcpy(pData, &MVP, sizeof(MVP));
// Flush the range of mapped buffer in order to
// make it visible to the device. If the memory
// is coherent (memory property must be
// VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) then the driver
// may take care of this, otherwise for non- coherent
// mapped memory vkFlushMappedMemoryRanges() needs
// to be called explicitly to flush out the pending
// writes on the host side
res = vkFlushMappedMemoryRanges(deviceObj->device, 1, &UniformData.mappedRange[0]);
assert(res == VK_SUCCESS);
}
在前面的实现中,一旦映射了 Uniform 缓冲区,我们就不会取消映射,直到应用程序停止使用 Uniform 缓冲区。 为了使 Uniform 缓冲区的范围对主机可见,我们使用 vkInvalidateMappedMemoryRanges()使映射范围无效。 在映射的缓冲区中更新新数据之后,主机使用 vkFlushMappedMemoryRanges()刷新任何挂起的写入操作,并使映射的内存对设备可见。
最后,不再需要映射的设备内存时,不要忘记使用 vkUnmapMemory()API 对其取消映射。 在本例中,我们在销毁 Uniform 缓冲区对象之前对其取消映射:
void VulkanDrawable::destroyUniformBuffer()
{
vkUnmapMemory(deviceObj->device, UniformData.memory);
vkDestroyBuffer(rendererObj->getDevice()->device, UniformData.buffer, NULL);
vkFreeMemory(rendererObj->getDevice()->device,
UniformData.memory, NULL);
}
以下是显示旋转立方体的输出:
Push constant updates Push 常量更新
Push 常量专门用于使用命令缓冲区更新着色器常量数据,而不是使用写入或复制描述符来更新资源。
注意
Push 常数提供了一条高速优化的线路来更新管线中的常量数据。
在本节中,我们将快速实现一个示例来演示 push 常量的用法。 我们将学习如何使用命令缓冲区利用 push 常量来更新着色器中的资源内容。 在片段着色器中,本示例在 push 常量 uniform 块 pushConstantsColorBlock 中定义了 constColor 和 mixerValue 两种资源类型。 constColor 资源包含一个整数值,该值用作以纯色(红色,绿色或蓝色)渲染旋转立方体的标志。 mixerValue 资源是一个与立方体颜色混合的浮点值。
Defining the push constant resource in the shader
在着色器中定义 Push 常量资源
着色器中的 push 常量资源是在布局中使用 push_constant 关键字定义的,该布局指示它是 push 常量块。 在下面的代码中,我们修改了现有的片段着色器并添加了两个常量变量,即 constColor 和 mixerValue。 如果 constColor 的值是 1,2 或 3,则渲染实体 solid 几何的颜色(红色,绿色或蓝色)。 否则,原始颜色就与 mixerValue 混合:
// Fragment shader
#version 450
layout (location = 0) in vec4 color; layout (location = 0) out vec4 outColor; layout(push_constant) uniform colorBlock {
int constColor; float mixerValue;
} pushConstantsColorBlock;
vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
vec4 green = vec4(0.0, 1.0, 0.0, 1.0);
vec4 blue = vec4(0.0, 0.0, 1.0, 1.0);
void main() {
if (pushConstantsColorBlock.constColor == 1) outColor = red;
else if (pushConstantsColorBlock.constColor == 2) outColor = green;
else if (pushConstantsColorBlock.constColor == 3) outColor = blue;
else
outColor = color*pushConstantsColorBlock.mixerValue;
}
在下一节中,我们将更新管线布局中的着色器 push 常量资源。
Updating the pipeline layout with the push constant
使用 push 常量更新管线布局
更新指示 push 常量范围的管线布局。 使用 VkPushConstantRange 结构在一个单独的管线布局中定义 push 常量范围。 还需要通知管线布局:管线的每个阶段可以访问多少个常量。
以下是这个结构的语法:
typedef struct VkPushConstantRange {
VkShaderStageFlags stageFlags;
uint32_t offset;
uint32_t size;
} VkPushConstantRange;
这里定义了 VkPushConstantRange 结构的各个字段:
字段 | 描述
—|---
stageFlags | 此字段指示此 push 常量范围所属的着色器阶段。 如果 stageFlags 未随着着色器阶段进行定义,则从该着色器阶段访问 push 常量资源成员就会产生一个实例,其中将来可能会读取到未定义的数据。
offset | 这是以字节为单位指定的 push 常量范围的起始偏移量,是 4 的倍数。
size | 该字段也是以字节为单位指定的,并且是 4 的倍数,表示 push 常量范围的大小。
使用 push 常量范围计数更新 VkPipelineLayoutCreateInfo 的 pushConstantRangeCount,以及使用一个指向 VkPushConstantRange 数组的指针更新 pPushConstantRanges:
void VulkanDrawable::createPipelineLayout()
{
// Setup the push constant range
const unsigned pushConstantRangeCount = 1;
VkPushConstantRange pushConstantRanges[pushConstantRangeCount]={}; pushConstantRanges[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; pushConstantRanges[0].offset = 0;
pushConstantRanges[0].size = 8;
// Create the pipeline layout with the help of descriptor layout. VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; pPipelineLayoutCreateInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pPipelineLayoutCreateInfo.pNext = NULL;
pPipelineLayoutCreateInfo.pushConstantRangeCount =
pushConstantRangeCount; pPipelineLayoutCreateInfo.pPushConstantRanges =
pushConstantRanges;
pPipelineLayoutCreateInfo.setLayoutCount =
(uint32_t)descLayout.size(); pPipelineLayoutCreateInfo.pSetLayouts = descLayout.data();
VkResult result;
result = vkCreatePipelineLayout(deviceObj->device, &pPipelineLayoutCreateInfo, NULL, &pipelineLayout);
assert(result == VK_SUCCESS);
}
Updating the resource data 更新资源数据
资源数据可以使用 vkCmdPushConstants()API 进行更新。 为了使用此 API,请分配一个名为 cmdPushConstant 的命令缓冲区,使用适当的值更新资源数据,然后执行 vkCmdPushConstants()。 以下是此 API 的语法:
void vkCmdPushConstants(
VkCommandBuffer commandBuffer,
VkPipelineLayout layout,
VkShaderStageFlags stageFlags, uint32_t offset,
uint32_t size,
const void* pValues);
这里定义了 vkCmdPushConstants 调用的各个参数:
参数 | 描述
—|---
commandBuffer | 这是要用来记录 push 常量更新的命令缓冲区对象(VkCommandBuffer)。
layout | 这是 VkPipelineLayout 对象,将被用来对 push 常量执行更新操作。
stageFlag | 这指定了在更新的范围内将要使用 push 常量的着色器阶段。 着色器阶段使用 VkShaderStageFlagBits 的位掩码来指示。
offset | 这是以字节为单位的起始偏移量,指定用于更新的 push 常量范围。
size | 这是指要更新的 push 常量范围的大小(以字节为单位)。
pValues | 这是一个包含若干新 push 常量值的数组。
push 常量的大小不得超过 VkPhysicalDeviceProperties :: limits :: maxPushConstantsSize 中指定的大小。
以下是 push 常量的实现,使用分配的命令缓冲区 cmdPushConstant 执行 push 常量的操作。 有两个 push 常量资源变量:constColorRGBFlag 和 mixerValue。 使用所需的数值对它们进行设置并在 vkCmdPushConstants()API 中指定:
void VulkanRenderer::createPushConstants()
{
// Allocate and start recording the push constant buffer.
CommandBufferMgr::allocCommandBuffer(&deviceObj->device,
cmdPool, &cmdPushConstant); CommandBufferMgr::beginCommandBuffer(cmdPushConstant);
enum ColorFlag { RED = 1,
GREEN = 2,
BLUE = 3,
MIXED_COLOR = 4,
};
float mixerValue = 0.3f; unsigned constColorRGBFlag = BLUE;
// Create push constant data, this contain a constant
// color flag and mixer value for non- const color unsigned pushConstants[2] = {}; pushConstants[0] = constColorRGBFlag;
memcpy(&pushConstants[1], &mixerValue, sizeof(float));
// Check if number of push constants does
// not exceed the allowed size
int maxPushContantSize = getDevice()->gpuProps.
limits.maxPushConstantsSize;
if (sizeof(pushConstants) > maxPushContantSize) { assert(0);
printf(“Push constant size is greater than expected, max allow size is %d”, maxPushContantSize);
}
for each (VulkanDrawable* drawableObj in drawableList)
{
vkCmdPushConstants(cmdPushConstant, drawableObj->pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT,
0, sizeof(pushConstants), pushConstants);
}
CommandBufferMgr::endCommandBuffer(cmdPushConstant); CommandBufferMgr::submitCommandBuffer(deviceObj->queue,
&cmdPushConstant);
}
以下是使用纯色渲染立方体的输出。 在此示例中,为了生成纯色,constColor 必须是 1,2 或 3:
以下是原始彩色立方体与 mixerValue 混合后的输出;对于此输出,constColor 不能是 1,2 或 3:
总结
在本章中,我们学习了描述符集的概念并理解了 Uniform 的实现。 我们渲染了一个多色立方体,并通过 uniform 块添加 3D 转换。 Uniform 是使用描述符集来实现的。 我们理解了描述符池的作用,并用它来分配描述符集对象。
我们使用管线布局将描述符集附加到图形管线;这允许管线(图形或计算机)访问描述符集。 另外,我们还了解并实现了 push 常量的更新,这是一种使用命令缓冲区更新常量数据的优化方法。
在下一章中,我们会使用纹理。 在 Vulkan 中通过图像资源类型实现纹理。 我们会介绍这种资源类型,并演示如何在渲染的几何图形上引入这些纹理。