使用暂存缓冲

修改createVertexBuffer函数,使用CPU可见的缓冲作为临时缓冲,使用显卡读取较快的缓冲作为真正的顶点缓冲:

void createVertexBuffer() {
    VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    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(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    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);
}

现在我们可以使用新的关联stagingBufferMemory作为内存的stagingBuffer缓冲对象来存放CPU加载的顶点数据。在本章节,我们会使用下面两个缓冲使用标记:

  • VK_BUFFER_USAGE_TRANSFER_SRC_BIT:缓冲可以被用作内存传输操作的数据来源。

  • VK_BUFFER_USAGE_TRANSFER_DST_BIT:缓冲可以被用作内存传输操作的目的缓冲。

vertexBuffer现在关联的内存是设备所有的,不能vkMapMemory函数对它关联的内存进行映射。我们只能通过stagingBuffer来向vertexBuffer复制数据。我们需要使用标记指明我们使用缓冲进行传输操作。

添加一个copyBuffer的辅助函数用于在缓冲之间复制数据:

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {

}

我们需要一个支持内存传输指令的指令缓冲来记录内存传输指令,然后提交到内存传输指令队列执行内存传输。通常,我们会为内存传输指令使用的指令缓冲创建另外的指令池对象,这是因为内存传输指令的指令缓存通常生命周期很短,为它们使用独立的指令池对象,可以进行更好的优化。我们可以在创建指令池对象时为它指定VK_COMMAND_POOL_CREATE_TRANSIENT_BIT标记。

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
    VkCommandBufferAllocateInfo allocInfo = {};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    VkCommandBuffer commandBuffer;
    vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
}

开始记录内存传输指令:

VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

vkBeginCommandBuffer(commandBuffer, &beginInfo);

我们之前对绘制指令缓冲使用的VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT标记,在这里不是必须的,这是因为我们只使用这个指令缓冲一次,等待复制操作完成后才继续程序的执行。我们可以使用VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT标记告诉驱动程序我们如何使用这个指令缓冲,来让驱动程序进行更好的优化。

VkBufferCopy copyRegion = {};
copyRegion.srcOffset = 0; // Optional
copyRegion.dstOffset = 0; // Optional
copyRegion.size = size;
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

我们使用vkCmdCopyBuffer指令来进行缓冲的复制操作。它以源缓冲对象和目的缓冲对象,以及一个VkBufferCopy数组为参数。VkBufferCopy数组指定了复制操作的源缓冲位置偏移,目的缓冲位置偏移,以及要复制的数据长度。和vkMapMemory指令不同,这里不能使用VK_WHOLE_SIZE来指定要复制的数据长度。

vkEndCommandBuffer(commandBuffer);

我们的内存传输操作使用的指令缓冲只包含了复制指令,记录完复制指令后,我们就可以结束指令缓冲的记录操作,提交指令缓冲完成传输操作的执行:

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;

vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
vkQueueWaitIdle(graphicsQueue);

和绘制指令不同,这一次我们直接等待传输操作完成。有两种等待内存传输操作完成的方法:一种是使用栅栏(fence),通过vkWaitForFences函数等待。另一种是通过vkQueueWaitIdle函数等待。使用栅栏(fence)可以同步多个不同的内存传输操作,给驱动程序的优化空间也更大。

vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

最后,传输操作完成后我们需要清除我们使用的指令缓冲对象。

接着,我们可以在createVertexBuffer函数中调用copyBuffer函数复制顶点数据到显卡读取较快的缓冲中:

createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);

copyBuffer(stagingBuffer, vertexBuffer, bufferSize);

最后,不要忘记清除我们使用的缓冲对象和它关联的内存对象:

...

    copyBuffer(stagingBuffer, vertexBuffer, bufferSize);

    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);
}

编译运行程序,确保一切正常。暂时由于我们使用的顶点数据过于简单,性能提升并不明显。当我们渲染更为复杂的对象时,可以看到更为明显的提升。