目的:暂存缓冲可以提升性能
现在我们创建的顶点缓冲已经可以使用了,但我们的顶点缓冲使用的内存类型并不是适合显卡读取的最佳内存类型。最适合显卡读取的内存类型具有 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 标记,含有这一标记的内存类型通常 CPU 无法直接访问。在本章节,我们会创建两个顶点缓冲。一个用于 CPU 加载据,一个用于显卡设备读取数据。我们通过缓冲复制指令将 CPU 加载到的缓冲中的数据复制到显卡可以快速读取的缓冲中去。
缓冲复制指令需要提交给支持传输操作的队列执行,我们可以查询队列族是否支持 VK_QUEUE_TRANSFER_BIT 特性,确定是否可以使用缓冲复制指令。对于支持VK_QUEUE_GRAPHICS_BIT 或VK_QUEUE_COMPUTE_BIT 特性的队列族,VK_QUEUE_TRANSFER_BIT特性一定被支持,所以我们不需要显式地检测队列族是否支持 VK_QUEUE_TRANSFER_BIT特性。
示例:
//创建顶点缓冲
void createVertexBuffer(){
VkDeviceSize bufferSize = sizeof(vertices[0])*vertices.size();
//使用 CPU 可见的缓冲作为临时缓冲,使用显卡读取较快的缓冲作为真正的顶点缓冲
VkBuffer stagingBuffer ;//缓冲对象存放 CPU 加载的顶点数据
VkDeviceMemory stagingBufferMemory ;//缓冲对象内存
/**
VK_BUFFER_USAGE_TRANSFER_SRC_BIT:缓冲可以被用作内存传输操作的数据来源。
VK_BUFFER_USAGE_TRANSFER_DST_BIT:缓冲可以被用作内存传输操作的目的缓冲
*/
createBuffer(bufferSize,VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer,stagingBufferMemory);
//将顶点数据复制到缓冲中
void* data;
/**
vkMapMemory 函数允许我们通过给定的内存偏移值和内存大小访问特定的内存资源。
这里我们使用的偏移值和内存大小分别时 0 和 bufferInfo.size。
还有一个特殊值 VK_WHOLE_SIZE 可以用来映射整个申请的内存。
vkMapMemory 函数的倒数第二个参数可以用来指定一个标记,暂不可用,必须将其设置为 0。
最后一个参数用于返回内存映射后的地址。
*/
//vkMapMemory将缓冲关联的内存映射到 CPU 可以访问的内存
vkMapMemory(device,stagingBufferMemory,0,bufferSize,0,&data);
/**
驱动程序可能并不会立即复制数据到缓冲关联的内存中去,
这是由于现代处理器都存在缓存这一设计,写入内存的数据并不一定在多个核心同时可见,
有下面两种方法可以保证数据被立即复制到缓冲关联的内存中去:
1.使用带有VK_MEMORY_PROPERTY_HOST_COHERENT_BIT属性的内存类型,
保证内存可见的一致性
2.写入数据到映射的内存后,调用 vkFlushMappedMemoryRanges 函数,
读取映射的内存数据前调用 vkInvalidateMappedMemoryRanges函数
第一种方法,它可以保证映射的内存的内容和缓冲关联的内存的内容一致。
但使用这种方式,会比第二种方式些许降低性能表现
*/
//将顶点数据复制到映射后的内存
memcpy(data,vertices.data(),(size_t)bufferSize);
//结束内存映射
vkUnmapMemory(device,stagingBufferMemory);
createBuffer(bufferSize,VK_BUFFER_USAGE_TRANSFER_DST_BIT|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
vertexBuffer,vertexBufferMemory);
/**
vertexBuffer 现在关联的内存是设备所有的,不能 vkMapMemory 函数
对它关联的内存进行映射。我们只能通过 stagingBuffer 来向 vertexBuffer复制数据。
我们需要使用标记指明我们使用缓冲进行传输操作.
*/
copyBuffer(stagingBuffer , vertexBuffer , bufferSize ) ;
//清除我们使用的缓冲对象和它关联的内存对象
vkDestroyBuffer(device , stagingBuffer , nullptr ) ;
vkFreeMemory(device , stagingBufferMemory , nullptr ) ;
}
//创建缓冲--方便地使用不同的缓冲大小,内存类型来创建我们需要的缓冲
//最后两个参数用于返回创建的缓冲对象和它关联的内存对象
void createBuffer(VkDeviceSize size,VkBufferUsageFlags usage,
VkMemoryPropertyFlags properties , VkBuffer& buffer,
VkDeviceMemory& bufferMemory){
//同createVertexBuffer基本相同
VkBufferCreateInfo bufferInfo = {};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
//指定要创建的缓冲所占字节大小
bufferInfo.size = size;
//指定缓冲中的数据的使用目的--这里存储顶点数据
bufferInfo.usage = usage;
//和交换链图像一样,缓冲可以被特定的队列族所拥有,也可以同时在多个队列族之前共享
//这里使用独有模式
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
//配置缓冲的内存稀疏程度
bufferInfo.flags = 0;
if(vkCreateBuffer(device,&bufferInfo,nullptr,
&buffer) != VK_SUCCESS){
throw std::runtime_error("failed to create buffer!");
}
/**
vkGetBufferMemoryRequirements 函数返回的 VkMemoryRequirements
结构体有下面这三个成员变量:
size:缓冲需要的内存的字节大小,它可能和 bufferInfo.size 的值不同
alignment:缓冲在实际被分配的内存中的开始位置。
它的值依赖于bufferInfo.usage 和 bufferInfo.flags。
memoryTypeBIts:指示适合该缓冲使用的内存类型的位域
*/
//获取缓冲的内存需求
VkMemoryRequirements memRequirements ;
vkGetBufferMemoryRequirements(device,buffer,&memRequirements);
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;//内存大小
/**
我们需要位域满足 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT(用于从 CPU 写入数据)
和 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT的内存类型
*/
allocInfo.memoryTypeIndex = findMemoryType(
memRequirements.memoryTypeBits,properties);
//分配内存
if(vkAllocateMemory(device,&allocInfo,nullptr,
&bufferMemory)!=VK_SUCCESS){
throw std::runtime_error("failed to allocate buffer memory!");
}
/**
第四个参数是偏移值。这里我们将内存用作顶点缓冲,可以将其设置为 0。
偏移值需要满足能够被 memRequirements.alignment 整除
*/
//将分配的内存和缓冲对象进行关联
vkBindBufferMemory(device,buffer,bufferMemory,0);
}
//用于在缓冲之间复制数据
void copyBuffer( VkBuffer srcBuffer , VkBuffer dstBuffer,
VkDeviceSize size){
/**
我们需要一个支持内存传输指令的指令缓冲来记录内存传输指令,
然后提交到内存传输指令队列执行内存传输。
通常,我们会为内存传输指令使用的指令缓冲创建另外的指令池对象,
这是因为内存传输指令的指令缓存通常生命周期很短,为它们使用独立的指令池对象,
可以进行更好的优化。
我们可以在创建指令池对象时为它指定VK_COMMAND_POOL_CREATE_TRANSIENT_BIT标记.
*/
VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
//分配指令缓冲对象
VkCommandBuffer commandBuffer;
if(vkAllocateCommandBuffers(device,&allocInfo,
&commandBuffer)!= VK_SUCCESS){
throw std::runtime_error("failed to allocate buffers!");
}
//开始记录内存传输指令
VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
//flags告诉驱动程序我们如何使用这个指令缓冲,来让驱动程序进行更好的优化
beginInfo.flags =
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if(vkBeginCommandBuffer(commandBuffer,&beginInfo)!=VK_SUCCESS){
throw std::runtime_error(
"failed to begin recording command buffer.");
}
//指定了复制操作的源缓冲位置偏移,目的缓冲位置偏移,以及要复制的数据长度
VkBufferCopy copyRegion = {};
copyRegion.srcOffset = 0;
copyRegion.dstOffset = 0;
copyRegion.size = size;//指定要复制的数据长度
//进行缓冲的复制
vkCmdCopyBuffer(commandBuffer,srcBuffer,dstBuffer,1,©Region);
//结束指令缓冲的记录操作,提交指令缓冲完成传输操作的执行
vkEndCommandBuffer( commandBuffer ) ;
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
vkQueueSubmit( graphicsQueue , 1 , &submitInfo , VK_NULL_HANDLE) ;
/**
有两种等待内存传输操作完成的方法:
1.一种是使用栅栏 (fence),通过 vkWaitForFences函数等待。
2.通过 vkQueueWaitIdle 函数等待。
使用栅栏 (fence) 可以同步多个不同的内存传输操作,给驱动程序的优化空间也更大
*/
vkQueueWaitIdle( graphicsQueue ) ;//等待传输操作完成
//清除我们使用的指令缓冲对象
vkFreeCommandBuffers( device ,commandPool ,1 ,&commandBuffer);
}
实际上,很少有程序为每个缓冲对象都调用 vkAllocateMemory 函数分配关联内存。物体设备允许同时存在的内存分配次数是有限制的,它最大为 maxMemoryAllocationCount。即使在高端硬件上,比如 NVIDIA GTX 1080,maxMemoryAllocationCount 也只有 4096 这么大。所以,通常我们会一次申请一个很大块的内存,然后基于这个内存实现自己的内存分配器为我们创建的对象通过偏移参数分配内存。
我们可以自己实现内存分配器,也可以使用 GPUOpen 提供的 VulkanMemoryAllocator 内存分配器。
在这里,我们的内存分配次数实际上很小,所以我们为每个需要内存的对象调用 vkAllocateMemory 函数分配内存